Definition
This is the hook you use when you want to attach state that lives outside React (router, global store, cache, etc.) to a component and keep it in sync with the screen. useSyncExternalStore synchronizes an external store into the React render tree. After a "the store changed" notification (the subscription callback) arrives, the value returned by getSnapshot() (a function that extracts the current state into a single value) is compared against the previous snapshot with Object.is. Object.is is JS's default equality comparison: for primitives (strings, numbers) it treats them as "equal" when the values match, and for objects only when they are the same reference. If they are judged equal, the re-render is skipped. So if the snapshot is "content changed but comparison-equal," subscribers miss the update forever. This is a problem of the identity of the snapshot value itself, distinct from shallow comparison (comparing object properties one level deep) or structural sharing (swapping only the changed part for a new object and reusing the rest).
Understanding in Context
To reduce row re-renders via per-id subscriptions, a row whose content did not change must have getSnapshot() return the same object reference as before so that React skips the re-render. Conversely, if the snapshot creates a new object every time, or the meaning is the same but the primitive value is Object.is-equal, you get a two-way trap: either the store changed but the screen didn't, or vice versa. It is best understood as a pair with read-model projection.
Why It's Needed
When attaching state that lives outside React—layout managers, focus stores, caches—to the UI, useSyncExternalStore is the standard path. If you put only a millisecond-resolution timestamp like new Date().toISOString() in the snapshot, then when a mount and an update happen consecutively within the same ms, the two snapshot strings become identical. React judges it "no change" and drops the re-render; the higher the load (test bursts, rapid consecutive mutations), the higher the reproduction rate, making it a load-dependent flake. In SSR, if getServerSnapshot is missing you get a "Missing getServerSnapshot" error on the server.
How It Works
- The component registers with the store via
subscribe(listener). - When the store changes,
listeneris called and React readsgetSnapshot()again. - If
Object.is(prevSnapshot, nextSnapshot)istrue, no re-render happens. - Even if the reference differs, it is skipped when the primitive value (string, number) is equal. If only
subscribefires but the snapshot doesn't change, the UI stays the same.
| Item | Role | Caveat |
|---|---|---|
| Snapshot comparison | Object.is(prev, next) | Equal value = no re-render |
| Monotonic revision | An always-increasing suffix like iso#1, iso#2 | Safe when consumers compare only the string |
getServerSnapshot | Initial snapshot for SSR/hydration | Errors on every server request if omitted |
subscribe | Calls the listener on change | No re-render if the snapshot doesn't change |
Practical Application
Use the timestamp only for UX display, and include a monotonically increasing counter (or a #revision suffix) in the snapshot as the identity key.
Components that go through SSR must always provide the third argument, getServerSnapshot. A regression test verifies that the snapshot string differs every time within the same tick / same ms burst (e.g., consecutive updates).
Tradeoffs
- The revision suffix is simple to implement and guarantees
Object.isfailure, but if the snapshot string mixes in the "display time," parsing becomes necessary. It is better to separate the display field from the identity key. - Using a new object reference as the snapshot on every mutation always re-renders, but the cost of unnecessary renders grows. A primitive + revision is usually cheaper.
When Not to Use It
- When the store is fully served by React state /
useReducer—external store subscription is unnecessary. - A design where the snapshot creates a new object on every render and thus is always a different reference—the benefit of the subscription model disappears and you only get a render storm.
Common Mistakes
- Making the snapshot depend only on timestamp / random / nondeterministic values.
subscribefires butgetSnapshotreturns the same string, so the UI "sometimes" doesn't change.- Forgetting
getServerSnapshoton the SSR path. - When failures correlate with load, writing it off as merely a "flaky test" and never suspecting snapshot identity.
Related Concepts
- ssr-hydration-mismatch — where server/client first-render consistency meets
getServerSnapshot. - focused-guest-editor — the same subscription model is often used when a focus store is kept as external state.