Hydration Proof

Search documentation

Find a page or section

useEffect and two-pass rendering

Render what the server rendered, then change it after hydration.

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:

components/greeting.tsx
"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.innerWidth or matchMedia() in render or in initial state;
  • new Date() or Math.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:

components/greeting.tsx
"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:

hooks/use-is-client.ts
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:

  1. Use useEffect instead when showing the first render without the measurement is acceptable.
  2. Render the component only on the client, with a fallback on the server (client-only components).
  3. Render it only after hydration, with an isMounted state set in an effect.
  4. Use useSyncExternalStore if 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 effectDo this
Screen size for layoutCSS media queries: render both layouts, let CSS pick (media query)
The current timePass the time the server used as a prop (time)
A saved preferenceStore it in a cookie and read it on the server (storage)
The themeSet 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:

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