useSyncExternalStore needs a getServerSnapshot, its third argument, to
render on the server and to hydrate, and that function must return the same
value in both places. require-stable-server-snapshot reports a missing
getServerSnapshot, and one that reads the browser, the clock or random
values.
| Rule | hydration-proof/require-stable-server-snapshot |
|---|---|
| What it reports | Require useSyncExternalStore to have a getServerSnapshot that returns the same value on the server and during hydration |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset (they never hydrate) |
| Suggestions | No |
| Options | none |
What it reports
useSyncExternalStore(subscribe, getSnapshot)without a third argument (or withundefined), includingReact.useSyncExternalStoreand theuse-sync-external-store/shimexport.- Inside
getServerSnapshot(an inline function, or a function declared in the same file and passed by name): any read of a browser-only global,localStorage/sessionStorage,matchMedia, the clock (Date.now(),new Date(), ...), random values (Math.random(), ...) and environment checks (typeof window).
Functions nested inside getServerSnapshot and functions imported from other
files are not inspected.
Why getServerSnapshot must be stable
During server rendering React calls getServerSnapshot; without it,
rendering the component on the server fails and React renders it again in the
browser (Missing getServerSnapshot
is that error). During hydration React calls getServerSnapshot again, in the
browser, and expects the value the server used. Only after hydration does it
switch to getSnapshot:
server: getServerSnapshot() → typeof window === 'undefined' → 'light'
hydration: getServerSnapshot() → typeof window === 'undefined' → 'dark' (mismatch)So getServerSnapshot must be a pure function of data both sides have.
Incorrect
function useOnline() {
return useSyncExternalStore(subscribe, () => navigator.onLine);
}
function useTheme() {
return useSyncExternalStore(
subscribeToStorage,
() => localStorage.getItem("theme"),
() => localStorage.getItem("theme") ?? "light"
);
}
const getWidth = () => window.innerWidth;
function useWidth() {
return useSyncExternalStore(subscribeToResize, getWidth, getWidth);
}Correct
function useOnline() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
}
function useTheme(serverTheme) {
return useSyncExternalStore(
subscribeToStorage,
() => localStorage.getItem("theme") ?? serverTheme,
() => serverTheme // read from a cookie on the server and passed as a prop
);
}
const getWidth = () => window.innerWidth;
const getServerWidth = () => 1024;
function useWidth() {
return useSyncExternalStore(subscribeToResize, getWidth, getServerWidth);
}Options
This rule has no options.
Messages
What ESLint prints for this rule, word for word:
useSyncExternalStorehas no getServerSnapshot. React needs it to render on the server and to hydrate: without it the server render errors and the component is rendered again in the browser. Pass a third argument that returns the value the server renders.- getServerSnapshot reads
<read>. It must return the same value on the server and while hydrating, so it cannot depend on the browser, the clock or randomness. Return a constant or data passed from the server.
When not to use it
When the store is only used in components that are never server-rendered.
Related
no-client-only-initial-state,no-storage-in-initial-renderandno-match-media-in-renderrecommenduseSyncExternalStorewith a server snapshot as a fix.- Missing getServerSnapshot: the React error, decoded
- localStorage and sessionStorage read during render,
the most common store behind
useSyncExternalStore