SSR-safe hooks in Next.js
The short answer
A hydration mismatch happens when the HTML React rendered on the server differs from what it renders on the client during hydration. It is almost always caused by reading browser-only state — localStorage, window dimensions, matchMedia, Date, or a random value — directly during render. The fix is to render the server-safe default first, then update after mount, either with a useEffect, an isClient flag, or a blocking inline script when you cannot tolerate a flash.
Server rendering gives you fast first paint and indexable HTML. It also gives you a category of bug that does not exist in a client-only app: the server and the browser disagree about what the page should look like.
The two errors and what each one means
"ReferenceError: window is not defined"
This one is a crash, not a warning. Your module or component touched a browser global while running in Node. It happens at import time if the access is at module scope, and at render time if it is in the component body.
"Hydration failed" / "Text content did not match"
This one is subtler. The code ran fine on both sides, but produced different output. React throws away the server HTML for that subtree and re-renders it on the client, which costs you the performance benefit you paid for and often shows a visible flash.
typeof window !== "undefined" stops the crash but causes the mismatch: it makes the render output depend on where the code is running, which is exactly what React forbids.Pattern 1: render the default, then correct after mount
The safest general-purpose fix. Initialise state to a value the server can also produce, then read the browser in an effect. Effects never run on the server, so the first render matches by construction.
Every browser-reading hook in this library follows exactly that shape, so you get the behaviour without writing it 20 times.
Hooks built on this pattern
Pattern 2: gate the whole subtree with an isClient flag
When a component genuinely cannot render meaningfully on the server — a map, a chart sized to the viewport, anything reading navigator — do not try. Render a placeholder of the same size, then swap.
Keep the placeholder the same size
Swapping a zero-height placeholder for real content is a layout shift, and Cumulative Layout Shift is a Core Web Vital. Reserve the final dimensions up front.Pattern 3: a blocking inline script when a flash is unacceptable
Patterns 1 and 2 both show the default first. For a theme that is usually fine for a chart, and completely unacceptable for the page background — a white flash before dark mode loads is the most-complained-about bug in dark-mode implementations.
The escape hatch is a small synchronous script in <head>. The browser executes it while parsing, before the first paint, so the correct class is already on <html> when anything is drawn.
suppressHydrationWarning on <html> is required here, and it is safe: it tells React to accept the DOM it finds at that one node rather than the server payload. It does not suppress warnings for the rest of the tree. This exact technique is what powers the theme switch on this site.Pattern 4: useIsomorphicLayoutEffect for measurement
React warns that useLayoutEffect does nothing on the server — correctly, since there is no layout to read. But downgrading to useEffect everywhere reintroduces flicker for genuine measurement work. The standard resolution is to pick per environment.
Server Components change where the problem lives
In the Next.js App Router, components are server components by default and never hydrate — so they cannot mismatch. The moment you add "use client", the component renders on the server for the initial HTML and hydrates in the browser, and every rule above applies again.
- Server Component — runs once, on the server. No hooks, no browser APIs, no hydration risk.
- Client Component — pre-rendered on the server, then hydrated. Hooks work; SSR safety matters.
- Push
"use client"as far down the tree as you can. Smaller client boundaries mean less to hydrate and fewer places to get it wrong.
Key takeaway
The rule that prevents every bug on this page: your render output must not depend on where the code is running. If a value only exists in the browser, it belongs in an effect or an inline script — never in the render body.
A checklist before you ship
- Search your client components for
window,document,localStorage,navigatorandmatchMediaoutside of effects. - Check for
Date.now(),new Date()andMath.random()in render — all three differ between server and client. - Run a production build, not just
next dev; some mismatches only surface once the HTML is actually pre-rendered. - Load the page with JavaScript disabled and confirm the server HTML is sensible on its own.
- Watch the console during a hard refresh — hydration warnings appear once and are easy to miss on a soft navigation.
Why does my hydration error only appear in production?
Development and production render through different code paths, and React batches and reports warnings differently. Time-dependent and locale-dependent values are also more likely to diverge once the server pre-renders at build time rather than per request. Always verify with next build && next start.
Is suppressHydrationWarning a legitimate fix?
On a single element whose content you deliberately correct with an inline script, yes — that is its intended use. As a way to silence a mismatch you have not understood, no: React will still discard and re-render that subtree on the client, so you keep the performance cost and lose the warning that told you about it.
Do I need these patterns with Vite or Create React App?
No. A purely client-rendered app has no server render to disagree with. These problems are specific to SSR and static pre-rendering — Next.js, Remix, Astro and Gatsby.
Does useEffect run on the server in Next.js?
No, never. Effects run only in the browser after hydration. That is precisely why moving browser access into an effect fixes the mismatch — the server simply skips it.
Keep reading
localStorage in React, done properly
Persisting React state to localStorage without hydration errors, cross-tab desync, quota crashes or JSON parse failures — with a production-ready hook.
Writing custom hooks that survive production
Naming, return shapes, dependency arrays, cleanup, testing and TypeScript patterns for custom React hooks that other people have to maintain.