A useEffect hydration error usually means code that belongs in an effect runs during render instead. Effects never run on the server, so the fix for most mismatches is two-pass rendering: render exactly what the server rendered first, then read the browser and update in useEffect once hydration is done.
Does useEffect run on the server?
No. React's docs say: "Effects only run on the client. They don't run during server rendering." (useEffect). The same is true of useLayoutEffect.
That makes the effect the one place where the server and the browser are allowed to differ. Everything else in a component, including the body of the function, useState initializers and useMemo, runs on the server and again in the browser during hydration, and must produce the same output both times.
What a useEffect hydration error really is
The error is rarely caused by an effect. It is caused by code that should have been in one:
"use client";
import { useState } from "react";
export function Greeting() {
// Runs on the server (no window: "Guest") and during hydration ("Ada").
const [name] = useState(() =>
typeof window === "undefined" ? "Guest" : localStorage.getItem("name")
);
return <p>Hello, {name}</p>;
}The typeof window check stops the server from crashing, but the two renders still disagree. Other forms of the same bug:
useState(typeof window !== "undefined")as an "is client" flag;window.innerWidthormatchMedia()in render or in initial state;new Date()orMath.random()in render.
Two-pass rendering with useEffect
Render the server's version first. After hydration, the effect runs, reads the browser, and sets state, which triggers a second render with the real value:
"use client";
import { useEffect, useState } from "react";
export function Greeting() {
const [name, setName] = useState("Guest"); // the same on both sides
useEffect(() => {
setName(localStorage.getItem("name") ?? "Guest");
}, []);
return <p>Hello, {name}</p>;
}This is the pattern React's hydrateRoot docs call "two-pass rendering". The first client render matches the server, so hydration succeeds, and "an additional pass will happen synchronously right after hydration."
The cost is real, and React's docs name it:
- Components render twice, which makes hydration slower.
- Users on slow connections see the first version for a while. The JavaScript can load seconds after the HTML, so a big change after hydration feels jarring.
Keep the first version close to the final one: a placeholder with the same size, not an empty space that jumps.
An isClient flag without an extra effect
When many components need to know whether they are past hydration, read it from useSyncExternalStore. React calls getServerSnapshot on the server and during hydration, and getSnapshot afterwards:
import { useSyncExternalStore } from "react";
const subscribe = () => () => {};
export function useIsClient() {
return useSyncExternalStore(
subscribe,
() => true, // in the browser, after hydration
() => false // on the server and during hydration
);
}false on the server and during hydration, true after it. Client-only components build a reusable ClientOnly wrapper on the same idea.
The useLayoutEffect warning
React 16 to 18 printed this warning when a component with useLayoutEffect rendered on the server:
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.React 19 removed it, but the reason for it stays: a layout effect measures the page before paint, and the server has no layout to measure. The useLayoutEffect docs suggest these fixes:
- Use
useEffectinstead when showing the first render without the measurement is acceptable. - Render the component only on the client, with a fallback on the server (client-only components).
- Render it only after hydration, with an
isMountedstate set in an effect. - Use
useSyncExternalStoreif the value comes from an external store.
When not to use two-pass rendering
Two-pass rendering fixes a useEffect hydration error by delaying the value. Often you can avoid the delay:
| Instead of reading in an effect | Do this |
|---|---|
| Screen size for layout | CSS media queries: render both layouts, let CSS pick (media query) |
| The current time | Pass the time the server used as a prop (time) |
| A saved preference | Store it in a cookie and read it on the server (storage) |
| The theme | Set the class with an inline script before hydration (theme) |
Catch it before it ships
The ESLint plugin treats effects as "not render" and everything else as render code, so it reports browser reads in the places that break hydration:
no-client-only-initial-state: browser values inuseState,useReduceranduseRefinitial values.no-window-render-branch:typeof windowchecks that change the output.no-storage-in-initial-renderandno-match-media-in-render: storage and media queries outside effects.
hydration-proof test finds the ones that reach a page, as HP1001 for text and HP1009 for an element only the client renders:
npx hydration-proof test