# This Suspense boundary received an update before it finished hydrating

> Suspense boundary received an update before it finished hydrating (#421): why an early update discards the server HTML, and the startTransition fix.

Source: https://hydration.jscrate.dev/docs/errors/suspense-boundary-received-update-before-hydrating
Last updated: 2026-09-18

"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:

```text
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:

```text
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:

```text
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 `localStorage` or a cookie, above a boundary that is still
  hydrating. See [storage](https://hydration.jscrate.dev/docs/causes/storage).
- **A store that rehydrates on load**, such as Zustand's `persist`, changing
  props passed into the boundary. See
  [the Zustand hydration error](https://hydration.jscrate.dev/docs/guides/zustand-hydration-error).
- **Lazy or streamed content**: `React.lazy`, `next/dynamic` or a streamed
  Server Component below the provider, which is exactly the kind of boundary
  that hydrates late.
- **Calling `root.render()`** on a root created by `hydrateRoot` before it has
  hydrated, which is #424.

## How to fix it

1. **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.
2. **Wrap the update in `startTransition`.** Before:

   ```tsx title="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:

   ```tsx title="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>
     );
   }
   ```

3. **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.
4. **Updates from `useSyncExternalStore` are 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](https://hydration.jscrate.dev/docs/issues/hp2005), for #421 and #424, and the boundary React
rendered again as [HP2003](https://hydration.jscrate.dev/docs/issues/hp2003). In React 19, where no error is
logged, the discarded server HTML is still reported, as
[HP1010](https://hydration.jscrate.dev/docs/issues/hp1010):

```bash
npx hydration-proof test --mode both
```

The ESLint rule
[`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/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`.

## Related

- [There was an error while hydrating (#422, #423)](https://hydration.jscrate.dev/docs/errors/there-was-an-error-while-hydrating)
- [Every minified React hydration code](https://hydration.jscrate.dev/docs/errors/minified-react-error-codes)
- [useEffect and two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering)
- [HP2005: an update arrived before hydration finished](https://hydration.jscrate.dev/docs/issues/hp2005)
- [localStorage and hydration](https://hydration.jscrate.dev/docs/causes/storage)
