Repository navigation
Read the store with useSyncExternalStore() - #6040
Open
Eugene (OEvgeny) wants to merge 18 commits into
Open
Eugene (OEvgeny) wants to merge 18 commits into
Eugene (OEvgeny) wants to merge 18 commits into
Conversation
added 18 commits
November 9, 2026 15:27
React 18 and later use their own hook. React 16/17 use a fallback that subscribes in one layout effect, which runs again only when subscribe changes.
useSelector(), <GraphProvider> and useKeyedStoreValue() now use the ponyfill. <GraphProvider> reads the graph and the Redux activities as one snapshot, instead of a store subscription while rendering plus two layout effects. It follows the store through the batched subscription of useSelector(), so React 16/17 re-render it in the same batch as the selectors. Selectors move to module scope, so getSnapshot() stays the same between renders.
createGraphFromStore() subscribed to the store when called and never unsubscribed. <GraphProvider> calls it while rendering, so every mount, and every render that never committed (e.g. the first one of <StrictMode>), left a store subscription behind. The graph now subscribes on its first subscriber, catches up with the store, and unsubscribes after its last one.
…ring <SuggestedActionsComposer> dispatched setRawState() while rendering. With useSyncExternalStore() on React 19, that dispatch notified components with a pending update, and React logged "Cannot update a component while rendering a different component". Dispatch from layout effects instead: the store still has the suggested actions by the time they show.
The page puts React 18 in window.React, and its default variant rendered with the bundled React DOM 16. React DOM 16 has no useSyncExternalStore(). The Fluent variants already render with createRoot() of React DOM 18.
Dictation progress sets the dictate state in Redux and dictateAbortable in a useState() of <Composer>. With useSyncExternalStore() on React 18 and later, the Redux state rendered first, at sync priority, and the microphone button was disabled while dictating until dictateAbortable rendered in the next task. dictateAbortable is now in a keyed store read with useSyncExternalStore(), so both render together.
The mock dispatched its events inside start(), which react-dictate-button calls from an effect. On React 18 and later, "starting" and "dictating" then render in the same frame, unlike with a browser's recognizer, and the forced-colors dictation snapshots differed by a few anti-aliased pixels.
The action sink sets the suggested actions while the store dispatches the incoming activity. With useSyncExternalStore() on React 18 and later, the activity rendered at sync priority and the suggested actions, a useState() of <SuggestedActionsComposer>, one task later. The suggested actions are now in a store read with useSyncExternalStore(), so both render together.
Waiting for one task did not always paint "starting" first: under load, a forced-colors dictation snapshot differed by a few pixels again (1 of about 40 runs).
When the host sets window.React but not window.ReactDOM, the IIFE bundle renders with its React DOM 16, which cannot run React's useSyncExternalStore(). Hide it from window.React there, so the ponyfill falls back as with React 16/17, like before. typingIndicator.scroll.html covers this setup again.
…yed store may notify
Eugene (OEvgeny)
marked this pull request as ready for review
November 10, 2026 04:38
Eugene (OEvgeny)
requested review from
William Wong (compulim),
Chris Whitten (cwhitten) and
Thomas Durnford (tdurnford)
as code owners
November 10, 2026 04:38
This branch has not been deployed
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.
Changelog Entry
Changed
useSyncExternalStore(), at sync priority on React 18 and later (with the IIFE bundle, whenwindow.ReactDOMis set too), in PR #6040, by @OEvgenyFixed
<StrictMode>:createGraphFromStore()now subscribes to the store only while its graph has subscribers, in PR #6040, by @OEvgenyDescription
Store reads use our
useSyncExternalStore()ponyfill: React's hook on 18+ (sync priority), 1 layout effect on 16/17.<GraphProvider>reads graph and Redux activities as one snapshot; unmounting leaves no subscription.Design
window.Reactbut nowindow.ReactDOMmay render with the bundled React DOM 16: hide React's hook there (no sync priority, as on main).createGraphFromStore()(core/graph.js) subscribes to the store on the firstgraph.subscribe().<GraphProvider>shares the batched subscription ofuseSelector()(1 render on 16/17).Specific Changes
react-hooks: the ponyfill (spec on 16.8.6, 18.3.1), keyed storesuseSelector(),GraphProvider.tsx,ReduxGraphProvider.tsx: use itdictateAbortable: keyed stores, render with their activity/stateisomorphic-react: hidesuseSyncExternalStorewithoutwindow.ReactDOMspeechPageObjects.js: events afterstart()and a frame, like browsers.changelog/<PR#>-<slug>.mdReview Checklist
z-index)package.jsonandpackage-lock.jsonreviewed