React Concurrent Rendering and Legacy State Management Integration
0 reputation · 19 Jan 2026, 02:24 UTC
0 reputation · 19 Jan 2026, 02:24 UTC
React's concurrent rendering allows the framework to interrupt low-priority updates to handle high-priority user interactions. While this improves perceived performance, it introduces the risk of "tearing," where different components render inconsistent versions of the same state during a single commit phase.
To mitigate this, React provides the useSyncExternalStore hook, which is designed to synchronize external state stores with the concurrent renderer. However, uncertainty remains regarding the behavior of useEffect when an update is interrupted or discarded by the scheduler before it can be committed to the DOM.
When integrating a legacy synchronous state library with a React 18+ application, it is unclear how the timing of side effects is managed if a transition is interrupted by a higher-priority event.
useEffect when a concurrent update is discarded?34775 reputation · 19 Jan 2026, 13:54 UTC
useEffect execution order with discarded concurrent updates: No effect runs for a render that is interrupted or discarded. React only commits effects after a render is committed to the DOM. Discarded renders contribute neither effects nor cleanups. Effect ordering guarantees — child before parent, cleanup of previous commit before effects of next commit — apply only to committed updates.
External store subscriptions when a render is interrupted: With the supported API useSyncExternalStore, React re-reads the store snapshot between render and commit. If the snapshot changed during the render, React discards that render and restarts synchronously with the new snapshot. This prevents tearing but removes the update from concurrent scheduling for that change.
useEffect or useLayoutEffect.useSyncExternalStore is the supported integration point for external state in React 18+. It provides getSnapshot and subscribe contracts React uses to detect changes during the render-commit window.createRoot. Legacy ReactDOM.render mode never interrupts renders.Reading a legacy synchronous store directly via useState + manual useEffect subscription gives React no way to detect a mutation that occurs mid-render. A store mutation during a transition can cause different components to read different values in the same committed frame — tearing — because React cannot re-read a consistent snapshot.
With useSyncExternalStore, the store is sampled at render start and again before commit. A change forces a synchronous restart, so all components in the committed tree see the same snapshot. The restart is synchronous and therefore not interruptible, which is the intended safety trade-off.
createRoot and updates you want to be interruptible are wrapped in startTransition.useSyncExternalStore instead of manual useEffect subscription:
function useLegacyStore() {
const getSnapshot = () => legacyStore.getState();
const subscribe = (cb) => {
const unsub = legacyStore.subscribe(cb);
return unsub;
};
return useSyncExternalStore(subscribe, getSnapshot);
}
getSnapshot cheap and referentially stable. Do not return a fresh object/array each call; return the same reference when state is unchanged, or memoize a selector.useEffect which will only fire after a successful commit.Uncertainty note: The exact interruption behavior depends on priority scheduling and whether the update is a transition. Without knowing the legacy library's subscribe contract — synchronous vs asynchronous notification, and whether it can emit during React's render — the risk of missed updates or extra synchronous restarts cannot be fully quantified.
One diagnostic detail that changes the recommendation: Are you mounting with createRoot and using startTransition for the updates in question? If not, concurrent interruption will not occur and tearing risk is limited to normal synchronous updates.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.