# useEffect and two-pass rendering

> A useEffect hydration error means browser-only code ran during render. Effects never run on the server, so render the server's output first, then update.

Source: https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering
Last updated: 2026-09-18

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](https://react.dev/reference/react/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:

```tsx title="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 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.innerWidth` or `matchMedia()` in render or in initial state;
- `new Date()` or `Math.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:

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

This is the pattern React's [hydrateRoot docs](https://react.dev/reference/react-dom/client/hydrateRoot) 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:

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

`false` on the server and during hydration, `true` after it. [Client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component) 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:

```text
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](https://react.dev/reference/react/useLayoutEffect) suggest these fixes:

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](https://hydration.jscrate.dev/docs/guides/client-only-component)).
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 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](https://hydration.jscrate.dev/docs/causes/media-query)) |
| The current time                | Pass the time the server used as a prop ([time](https://hydration.jscrate.dev/docs/causes/time))                            |
| A saved preference              | Store it in a cookie and read it on the server ([storage](https://hydration.jscrate.dev/docs/causes/storage))               |
| The theme                       | Set the class with an inline script before hydration ([theme](https://hydration.jscrate.dev/docs/causes/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`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state): browser values in `useState`, `useReducer` and `useRef` initial values.
- [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch): `typeof window` checks that change the output.
- [`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render) and [`no-match-media-in-render`](https://hydration.jscrate.dev/docs/rules/no-match-media-in-render): storage and media queries outside effects.

`hydration-proof test` finds the ones that reach a page, as [HP1001](https://hydration.jscrate.dev/docs/issues/hp1001) for text and [HP1009](https://hydration.jscrate.dev/docs/issues/hp1009) for an element only the client renders:

```bash
npx hydration-proof test
```

## Related

- [What is hydration in React?](https://hydration.jscrate.dev/docs/guides/what-is-hydration)
- [Client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component)
- [localStorage hydration errors](https://hydration.jscrate.dev/docs/causes/storage)
- [Browser-only APIs during render](https://hydration.jscrate.dev/docs/causes/browser-api)
- [Missing getServerSnapshot](https://hydration.jscrate.dev/docs/errors/missing-getserversnapshot)
