React Error #421: This Suspense boundary received an update before it finished hydrating
Suspense updated before hydrating
Verified against React 19 source (ReactFiberHydrationContext.js), React docs: hydration, Next.js docs: streaming · Updated May 2026
> quick_fix
A state update fired against a Suspense boundary before React finished hydrating it. The fix: defer client-only updates until after mount with useEffect, or wrap them in startTransition so React waits for hydration before applying.
import { useEffect, useState } from 'react'
function Theme({ children }) {
const [theme, setTheme] = useState('light') // SSR default
useEffect(() => {
// Read browser-only value AFTER hydration
setTheme(localStorage.getItem('theme') ?? 'light')
}, [])
return <Suspense fallback={<Spinner />}>
<ThemedContent theme={theme}>{children}</ThemedContent>
</Suspense>
}What causes this error
When SSR HTML is being hydrated by the client, React replays the render to attach event listeners. If a state update fires during that window (from a useEffect, a same-tick setState, or a router event), and that update reaches a Suspense boundary, React can't safely apply it - hydration must complete first. React throws #421 to say: this update arrived too early.
How to fix it
- 01
step 1
Find the update source
The trace points to the Suspense boundary, but the bug is wherever the update came from. Look for setState calls in render bodies, useLayoutEffect that fires before hydration completes, or router-event subscriptions that call setState immediately on mount.
// Common offenders function Bad() { const router = useRouter() setSomething(router.path) // setState during render return <Suspense>...</Suspense> } - 02
step 2
Move client-only state into useEffect
If the value comes from window, localStorage, or any browser-only API, it cannot be set during SSR. Initialize with the SSR default, then update in useEffect after mount.
function ClientOnly() { const [width, setWidth] = useState(0) // SSR default useEffect(() => { setWidth(window.innerWidth) const handler = () => setWidth(window.innerWidth) window.addEventListener('resize', handler) return () => window.removeEventListener('resize', handler) }, []) return <p>{width}</p> } - 03
step 3
Wrap router-driven updates in startTransition
If you subscribe to router events that fire immediately on mount (Next.js App Router does this), the resulting setState may race hydration. Wrap in startTransition so React defers it until after hydration.
import { startTransition, useEffect } from 'react' function Tracker() { const path = usePathname() useEffect(() => { startTransition(() => { analytics.track('pageview', path) }) }, [path]) } - 04
step 4
Remove setState in render bodies
Calling setState during render schedules an update synchronously. During hydration, this update can reach a Suspense boundary before hydration completes. Move to useEffect or compute the value derived from props directly.
// Bad - setState in render function Bad({ user }) { const [name, setName] = useState('') if (user && !name) setName(user.name) // hits #421 during hydration return <Suspense>...</Suspense> } // Good - derive directly function Good({ user }) { const name = user?.name ?? '' return <Suspense>...</Suspense> } - 05
step 5
Check third-party providers that mount listeners early
Auth providers, analytics SDKs, and feature-flag clients often fire their first event in their constructor or top-level useEffect. If these trigger setState in your tree mid-hydration, you'll see #421. Lazy-init or wrap their callbacks in startTransition.
useEffect(() => { const sub = featureFlags.subscribe((flag) => { startTransition(() => setFlag(flag)) }) return () => sub.unsubscribe() }, [])
Why #421 happens at the runtime level
Hydration in React 19 is incremental: each Suspense boundary hydrates independently as its content becomes ready. ReactFiberHydrationContext.js tracks a hydrating flag per fiber. When an update is scheduled, the reconciler walks up to find the nearest Suspense ancestor; if that ancestor is still hydrating, the update cannot be applied without breaking the SSR alignment. React throws invariant #421 in this case. The check fires before any DOM mutation, so the SSR HTML stays intact. The error specifically guards the contract that hydration completes against the original server output before any client-driven divergence is allowed.
Common debug mistakes for #421
- Reading window/localStorage/document directly in render and calling setState with the result.
- Subscribing to router events that fire synchronously on mount and calling setState in their callback.
- Mounting analytics or feature-flag SDKs whose constructors trigger setState as a side effect.
- Using useLayoutEffect (which runs synchronously after render but during hydration) to set state on a Suspense-wrapped child.
- Migrating an SSR component to streaming without auditing which effects depend on hydrated state.
When #421 signals a deeper problem
Repeated #421 errors signal that the component tree has implicit ordering dependencies that hydration exposes. The architectural fix is to separate three phases explicitly: SSR render (no browser APIs), hydration (no setState), and post-mount (effects can mutate state). Components that need browser data should use a useState default + useEffect-update pattern uniformly. For analytics and feature flags, lazy-load them after a useEffect tick rather than at the module top level. In Next.js, prefer server components for layout and only use 'use client' for the leaf components that genuinely need interactivity, minimising the surface where hydration timing matters.
Editor's take
This error has a signature timing: it surfaces most reliably during a midnight deploy when a small startup's frontend team pushes a React 19 upgrade alongside a new Suspense-wrapped data-fetching layer. The CI pipeline is green, Storybook looks fine, and then production gets 10x the usual traffic from a Product Hunt launch — suddenly every page with a Suspense boundary that wraps a client-side analytics init throws #421 in a cascade. The error is latent in development because Vite's dev server skips SSR hydration entirely, so it only manifests under the exact conditions you can't easily reproduce locally.
Seeing this error on a résumé is a soft signal that the engineer has crossed from "React user" to "React internals reader." Junior developers hit #421 without understanding why — they cargo-cult a useEffect wrapper and move on. The engineer who actually fixes it has read ReactFiberHydrationContext.js, understands that React 19 hydrates incrementally per boundary rather than atomically, and knows the difference between deferring an effect and wrapping a dispatch in startTransition. That conceptual gap — synchronous mental model versus incremental hydration reality — is exactly what separates mid-level from senior on any team running SSR at scale.
In the same incident you will almost always find React Error #425 (text content mismatch) appearing first in the console, followed by #418 if the hydration failure is severe enough to force a full client re-render. Upstream, look for NEXT_REDIRECT thrown inside a Suspense boundary or a zustand store initialized before the React tree mounts — both trigger the premature update that causes #421. The combination of #425 + #421 + a blank flash on first load is a reliable fingerprint for this class of hydration ordering bug.
By Bikram Nath · Curator · Updated May 2026
Frequently asked questions
Why does React reject updates during hydration?
Hydration must produce a tree that exactly matches the SSR HTML, then attach event listeners. A state update mid-hydration would change the tree before React has finished comparing it to the existing DOM, breaking the alignment. React's solution is to throw #421 if an update reaches a Suspense boundary before its hydration completes. The right fix is to ensure client-only updates fire after hydration, either via useEffect (which runs after) or startTransition (which is deferred behind committed work).
Is #421 the same as #418 or #425?
No. #418 (hydration mismatch) means the SSR HTML differed from what the client would render at t=0. #425 (text mismatch) is the specific text-content version of #418. #421 is different: SSR and client markup may agree, but a state update arrived before React could finish attaching listeners. The fix patterns overlap (move browser-only code to useEffect) but the root cause differs: #418 is about render output; #421 is about update timing.
Does using 'use client' fix this?
Partially. 'use client' marks a component as needing client-side hydration; without it, the component is server-only and never hydrates. But within a client component, the same #421 rules apply: updates that fire mid-hydration still throw. Use 'use client' to opt into hydration, then ensure your client component's effects don't trigger setState before hydration completes. The two concerns are separate.
How does Next.js App Router interact with #421?
App Router uses Suspense extensively for streaming and partial hydration. Each loading.js boundary creates a Suspense. If your client component subscribes to router events (usePathname, useSearchParams) and immediately calls setState, you can hit #421 during navigation. The pattern that works: read the router value during render to derive UI, wrap any side-effect-triggered updates in startTransition or useEffect. Server components can't trigger #421 because they don't hydrate.