[DIP] useSyncExternalStore

Tech

I was shared, internally at work, some research on Tearing — a visual inconsistency phenomenon that occurs in React's Concurrent Mode rendering behavior. Tearing is a phenomenon that describes a situation where, while rendering proceeds and new state is reflected due to the use of concurrency features such as startTransition and Suspense, the parts that retain the previous state become visually inconsistent.

Hearing how React solved the problem (Tearing) caused by concurrency features by providing useSyncExternalStore, I became curious about the internal implementation of useSyncExternalStore and wanted to look into its internal logic.

You can find the details about Tearing in the summary in the react-18 Discussions below, and also in the Concurrent Mode talk video.



useSyncExternalStore

In the React v19 official documentation, useSyncExternalStore is introduced simply as a React Hook that lets you subscribe to an external store.

tsx
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

The functional aspects of useSyncExternalStore are well summarized in the React v19 official documentation, so I'll jump straight into the internal logic.

useSyncExternalStore-1



useSyncExternalStoreShim.js

*
v19.1.0

useSyncExternalStore is divided into Client, Server, and BuildInAPI.


It starts from useSyncExternalStoreShim, where the logic to use is determined based on whether it's a server environment and whether useSyncExternalStore exists as a built-in API.

tsx
import {useSyncExternalStore as client} from './useSyncExternalStoreShimClient'; import {useSyncExternalStore as server} from './useSyncExternalStoreShimServer'; import {isServerEnvironment} from './isServerEnvironment'; import {useSyncExternalStore as builtInAPI} from 'react'; const shim = isServerEnvironment ? server : client; export const useSyncExternalStore: <T>( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ) => T = builtInAPI !== undefined ? builtInAPI : shim;

The part I wanted to check here was how the problem caused by concurrency features (Tearing) was solved, so I looked at ShimClient and the useSyncExternalStore that exists as the built-in API within React.



useSyncExternalStoreShimClient.js

*
v19.1.0

The ShimClient version is described as logic that exists to be used when a React version has useSyncExternalStore but the builtInAPI is not available.


I've extracted the logic from useSyncExternalStoreShimClient that I thought was important. Most of the logic has comments describing what each operation does.

tsx
export function useSyncExternalStore<T>( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T { ... const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}}); useLayoutEffect(() => { inst.value = value; inst.getSnapshot = getSnapshot; if (checkIfSnapshotChanged(inst)) { forceUpdate({inst}); } }, [subscribe, value, getSnapshot]); ... };

The logic itself is simple: when the subscribed state information has changed, it compares it with the previous state (checkIfSnapshotChanged), and if there is a change, it calls forceUpadate to trigger a re-render.

*
checkIfSnapshotChanged : compares using Object.is

useLayoutEffect runs synchronously and, in React's rendering process, is handled in the Commit Phase after the Render Phase, performing its operation before the actual browser paint.


useSyncExternalStore stores the passed snapshot as the current value when rendering starts, and in useLayoutEffect, before rendering completes, it compares the stored snapshot with the current snapshot; if they are not the same state, it re-renders again starting from the Render Phase so that it is rendered with the correct value.



useSyncExternalStore as builtInAPI

In the builtInAPI logic as well, I approached and examined only the important parts. In the end, as in the logic seen above, in the pre-render situation you need processing that checks the state value and confirms whether it will render with the correct value, and if it's an incorrect value, performs a synchronous update to correct it before it's actually reflected in the browser.


The useSyncExternalStore buildInAPI is divided into mount and update.

tsx
const HooksDispatcherOnMount: Dispatcher = { ... useSyncExternalStore: mountSyncExternalStore, ... }; const HooksDispatcherOnUpdate: Dispatcher = { ... useSyncExternalStore: updateSyncExternalStore, ... };


mountSyncExternalStore

I confirmed that, in Concurrent Mode since React 18, the processing that guarantees consistent rendering for state values is coupled with the Fiber rendering system.


In the Render Phase, during mount and update, the pushStoreConsistencyCheck function schedules a snapshot for a consistency check, and later, if the value is judged to be incorrect, it performs a re-render.

tsx
function mountSyncExternalStore<T>( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T { ... const rootRenderLanes = getWorkInProgressRootRenderLanes(); if (!includesBlockingLane(rootRenderLanes)) { pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot); } ... };


updateSyncExternalStore

In the update logic, in addition to the pushStoreConsistencyCheck handling, it also performs direct verification of snapshot changes, and when a change is needed, it marks the currently working Fiber with an update for re-rendering (markWorkInProgressReceivedUpdate). The reason is, naturally, that in the mount stage there is not yet a previous snapshot to compare against, so it only performs scheduling for the consistency check, while in the update stage it compares against the previous snapshot in advance and marks it for re-rendering, so that the re-render is triggered before scheduling the consistency check, avoiding unnecessary work.


tsx
function updateSyncExternalStore<T>( subscribe: (() => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T { ... const prevSnapshot = (currentHook || hook).memoizedState; const snapshotChanged = !is(prevSnapshot, nextSnapshot); if (snapshotChanged) { hook.memoizedState = nextSnapshot; markWorkInProgressReceivedUpdate(); } ... if (!isHydrating && !includesBlockingLane(renderLanes)) { pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot); } ... };


pushStoreConsistencyCheck

Snapshots that need scheduling are stored in a queue. The queue stores the snapshot at render time and getSnapShot() so that the snapshot before the Commit Phase stage can be obtained.

tsx
function pushStoreConsistencyCheck<T>( fiber: Fiber, getSnapshot: () => T, renderedSnapshot: T, ): void { ... const check: StoreConsistencyCheck<T> = { getSnapshot, value: renderedSnapshot, }; ... componentUpdateQueue.stores = [check]; ... }


isRenderConsistentWithExternalStores

The processing that actually compares the scheduled snapshots is done through isRenderConsistentWithExternalStores, which performs the consistency verification by comparing the snapshot stored in the queue with the current snapshot obtained via getSnapShot().

tsx
function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean { ... const getSnapshot = check.getSnapshot; const renderedValue = check.value; try { if (!is(getSnapshot(), renderedValue)) { return false; } ... }


Summary

Let me summarize the flow as I understood it.

  • useSyncExternalStore is a React Hook that lets you subscribe to an external store, serving to help prevent state inconsistencies caused by concurrency features.
  • Before Concurrent Mode, it uses useLayoutEffect to check for state inconsistency before the browser paint, and re-renders if there is an inconsistency.
  • In Concurrent Mode, it checks for state inconsistency in the Render Phase within the Fiber rendering system and re-renders.

If you think about it simply, it may seem like the same thing repeated… but you could say it re-checks the state to be rendered right before the state is reflected in the browser, and re-renders if there is an inconsistency.

A thought I had while analyzing this logic was that it reminded me of when I developed and maintained a collaborative real-time editing service in the past. It was a service that included correction processing to show the same result within a screen edited by multiple people simultaneously; the target being shown on screen and the correction algorithm are different, but I felt that the effort to show users an accurate result is the same. If I ever face a state-inconsistency problem caused by concurrency features like the above within React, I imagine that, based on my past experience, a different approach might also be possible.

useSyncExternalStore-2