A useEffect hydration error usually starts before the effect runs. A component reads a browser-only value during render, so the server and browser produce different HTML. Render the same initial value on both sides, then read the browser in useEffect after hydration.
Does useEffect run on the server?
No. useEffect and useLayoutEffect do not run during server rendering (React's useEffect reference).
The component body, useState initializers, and useMemo can run on both sides. Their output must agree for the first browser render. An effect runs later, after hydration, so it can safely read browser-only state.
What a useEffect hydration error really is
Here, the storage read happens in a state initializer, not an effect:
"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 prevents a server crash. It does not make the initial values match. Watch for the same pattern in:
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
Start with the same value on the server and in the browser. After hydration, read storage in an effect and update the state:
"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>;
}React calls this two-pass rendering. The first browser render matches the server. The effect then triggers another render with the saved name.
There is a tradeoff:
- The component renders again after hydration.
- People may see the initial value until JavaScript loads and the effect runs.
Choose an initial value or placeholder that will not make the layout jump when it changes.
An isClient flag without an extra effect
If several components need a shared "has hydrated" value, you can use useSyncExternalStore. Its server snapshot is also used during hydration; its browser snapshot is read 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
);
}This hook returns false for the server and the first browser render, then true. See client-only components for a reusable wrapper.
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 this warning. The underlying limitation remains: the server has no layout to measure. The useLayoutEffect docs suggest these options:
- 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 is useful when the value exists only in the browser. If the server can know it, or CSS can handle the difference, you may not need a second render:
| 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 looks for browser-only reads during render, including state initializers:
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 checks the page in a browser. It reports changed text as HP1001 and a client-only element as HP1009:
npx hydration-proof test