fix: pace useObservable emission delivery to React's render cycle - #490
Draft
bjoerge wants to merge 2 commits into
Draft
fix: pace useObservable emission delivery to React's render cycle#490bjoerge wants to merge 2 commits into
bjoerge wants to merge 2 commits into
Conversation
React restarts an in-flight concurrent render pass whenever a useSyncExternalStore snapshot changes mid-pass, so a source that emits faster than the pass can complete blocks it from ever committing. useObservable now delivers the first emission immediately and, while a delivered value is still rendering, holds newer emissions and delivers only the latest once the main thread is idle (requestIdleCallback; setTimeout(0) fallback). This bounds restarts to one per commit cycle. Isolated emissions and synchronous first values at mount are unchanged. The paced branch keeps its own snapshot state, since the uSES consistency check re-reads the snapshot mid-pass; it falls back to the live state until the first paced delivery and resets on disconnect. Contract change: bursts coalesce, so intermediate values may never render through useObservable. useSyncObservable still delivers every emission.
🦋 Changeset detectedLatest commit: da82695 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9e26daf. Configure here.
A synchronously completing multi-value source (of(a, b)) flashed on mount: the render-phase warm-up painted the last value, the shared pipeline reset on complete, and at commit the replay delivered its first value as the paced leading edge — painting b, then a, then b again at idle. Caught by Cursor Bugbot on #490. Pacing within a single task gains nothing: all same-task notifications produce one scheduled render and React paints only the last value, so holding same-task emissions exposes an earlier value and delays the final one. The operator (now hand-rolled, replacing the exhaustMapWithTrailing composition) delivers emissions synchronously while in the same microtask as the last delivery and holds only emissions arriving later — the actual starvation vector. Same-task behavior (mount replays, event-handler bursts) is identical to unpaced delivery again, so the earlier test edits accounting for same-task coalescing are reverted. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

React restarts an in-flight concurrent render pass whenever a useSyncExternalStore snapshot changes mid-pass, so a source that emits faster than the pass can complete blocks it from ever committing.
useObservable now delivers the first emission immediately and, while a delivered value is still rendering, holds newer emissions and delivers only the latest once the main thread is idle (
requestIdleCallbackwith setTimeout(0) fallback). This bounds restarts to one per commit cycle. Isolated emissions and synchronous first values at mount are unchanged. The paced branch keeps its own snapshot state, since the uSES consistency check re-reads the snapshot mid-pass; it falls back to the live state until the first paced delivery and resets on disconnect.Contract change: bursts coalesce, so intermediate values may never render through useObservable. useSyncObservable still delivers every emission.
Note
Medium Risk
Changes core react-rx subscription timing and observable delivery semantics for all useObservable consumers; behavior is well-tested but coalescing can surprise code that relied on every emission rendering.
Overview
Fixes concurrent render starvation when observables emit faster than React can finish a pass: each
useSyncExternalStoresnapshot change mid-pass forces a restart, so rapid emissions could block commits indefinitely.useObservablenow uses a render-paced branch (paceToRenderIdle+requestIdleCallback, withsetTimeout(0)fallback): same-microtask bursts still pass through; cross-task emissions while a value may still be rendering are coalesced to the latest and delivered after render-idle.useSyncObservablestays on the live branch with every emission.The shared cache splits live vs paced snapshot state so uSES consistency checks do not observe newer live values during an in-flight paced delivery; paced state resets on disconnect so remounts do not show stale values.
Contract: intermediate values in cross-task bursts may never render through
useObservable— useuseSyncObservablewhen every emission must be synchronous. Minor version bump documents the behavior change.Reviewed by Cursor Bugbot for commit da82695. Bugbot is set up for automated code reviews on this repo. Configure here.