"This Suspense boundary received an update before it finished hydrating" is a
React 18 error (#421): a state or context change reached a <Suspense>
boundary whose server HTML React had not hydrated yet, so React discarded that
HTML and rendered the boundary in the browser. Wrap the update in
startTransition, or make it after hydration.
The error
React 18 in development:
This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.In production:
Minified React error #421; visit https://reactjs.org/docs/error-decoder.html?invariant=421 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.The root has its own version, error #424, when the page is updated before any of it has hydrated:
This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.React 19 no longer reports #421. It still tries to hydrate the boundary before applying the update, and when it cannot, it renders the boundary on the client without a message. The cost is the same; only the error is gone. #424 still exists in React 19.
Why a Suspense boundary received an update before it finished hydrating
React hydrates a page in pieces. The shell hydrates first, then each
<Suspense> boundary when its code and data are ready. Until then, the
boundary is still the server's HTML, with no components behind it.
Effects in the already-hydrated part run in the meantime. If one of them sets state or changes a context value that the waiting boundary uses, React has an update for components that do not exist yet. It first tries to hydrate the boundary right away. If the boundary is still waiting, for example for a lazy component's code, React throws the server HTML away and renders the boundary from scratch, with its fallback first. Users see the content disappear and come back.
A startTransition update is different: React may keep showing the server
HTML, finish hydrating the boundary, and apply the update afterwards.
Common causes
- A provider that sets state in an effect on load: a user, theme or cart
context read from
localStorageor a cookie, above a boundary that is still hydrating. See storage. - A store that rehydrates on load, such as Zustand's
persist, changing props passed into the boundary. See the Zustand hydration error. - Lazy or streamed content:
React.lazy,next/dynamicor a streamed Server Component below the provider, which is exactly the kind of boundary that hydrates late. - Calling
root.render()on a root created byhydrateRootbefore it has hydrated, which is #424.
How to fix it
-
Find the update. The component stack of the error points at the boundary. Look above it for effects that call a state setter on mount, and for store subscriptions.
-
Wrap the update in
startTransition. Before:app/user-provider.tsx "use client"; import { createContext, useEffect, useState, type ReactNode } from "react"; export const UserContext = createContext<string | null>(null); export function UserProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<string | null>(null); useEffect(() => { setUser(localStorage.getItem("user")); }, []); return ( <UserContext.Provider value={user}>{children}</UserContext.Provider> ); }After:
app/user-provider.tsx "use client"; import { createContext, startTransition, useEffect, useState, type ReactNode, } from "react"; export const UserContext = createContext<string | null>(null); export function UserProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<string | null>(null); useEffect(() => { startTransition(() => { setUser(localStorage.getItem("user")); }); }, []); return ( <UserContext.Provider value={user}>{children}</UserContext.Provider> ); } -
Keep the value stable during hydration when you can. A value the server can know (a cookie) needs no update at all: read it on the server and pass it down.
-
Updates from
useSyncExternalStoreare always synchronous, and a transition does not change that. Delay the store change until the page has hydrated, or keep the changed value out of the waiting boundary's props.
Find every instance
hydration-proof reports an update that arrived before hydration finished as HP2005, for #421 and #424, and the boundary React rendered again as HP2003. In React 19, where no error is logged, the discarded server HTML is still reported, as HP1010:
npx hydration-proof test --mode bothThe ESLint rule
no-storage-in-initial-render
points you to reading storage in an effect instead of during render; the
update that effect makes is the one to wrap in startTransition.