Fix React 18 Hydration Mismatch in Next.js
A step‑by‑step fix for the React 18 hydration mismatch error in Next.js apps, covering root cause, code changes, verification, and prevention.
Photo by Gabriel Heinzer on Unsplash
Right after the first paint, the browser console fills with this:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
This may be caused by a bug in React, or a mismatch between the server and client HTML.
See https://react.dev/link/hydration-mismatch for more information.It shows up on pages using the Next.js App Router (or Pages Router) with React 18, and it's reproducible on every local dev run, on Vercel preview deployments, and even after a fresh next build. The error is agnostic to environment — it appears in Chrome, Firefox, and Safari alike — because the underlying cause is purely a markup difference, not a browser quirk.
Almost always, the markup difference comes from non-deterministic rendering: random IDs, Date.now(), or browser-only globals producing different output on the server versus the client. The fix is to move that logic into a client-only effect or use stable React APIs so the two renders match exactly.
How React 18 decides your page is broken#
In React 18 the hydration algorithm walks the server‑rendered DOM and the client‑side virtual DOM simultaneously, comparing each node during reconciliation. When they diverge, React aborts hydration and logs the error above. Even a single differing text node or attribute value is enough to trigger the mismatch, because the reconciler compares them node‑by‑node rather than as a string.
Next.js renders components on the server first, then streams the HTML to the browser. When the client mounts, React re‑executes the component function. If anything in the function body yields a different result the second time, the server and client produce different nodes. Typical culprits:
- Date/Time calls (
new Date(),Date.now()) that embed the current timestamp. - Random values (
Math.random(),crypto.getRandomValues) used for IDs or keys. - Browser‑only globals (
window,document,localStorage) accessed directly in the component body. - Third‑party UI libraries that inject unique IDs on mount (e.g., Material‑UI, Ant Design) without a deterministic fallback.
Reproducing it with one Math.random() call#
This component triggers the error every time:
// components/RandomBadge.tsx
import React from 'react';
export default function RandomBadge() {
// ❌ This runs on both server and client, producing different IDs each time
const id = Math.random().toString(36).substring(2, 9);
return (
<span id={`badge-${id}`} className="badge">
Random ID: {id}
</span>
);
}When the server renders this component, it generates an ID like badge-3f9a2c1. The client, a few milliseconds later, generates badge-7b1e4d9. The mismatch triggers the hydration error.
The deterministic rewrite: useId#
// components/RandomBadge.tsx
'use client'; // Marks this as a Client Component — still prerendered to HTML on the server, then hydrated on the client
import React, { useId } from 'react';
export default function RandomBadge() {
// ✅ useId returns a stable identifier that matches across server and client
const id = useId(); // e.g. "r:1"
return (
<span id={`badge-${id}`} className="badge">
Random ID: {id}
</span>
);
}Why this works: useId is a React 18 hook that returns the same identifier on the server and the client, so the prerendered HTML and the hydrated DOM agree. Marking the component 'use client' is necessary here because useId is a hook, but it does not skip server‑side prerendering — the <span> is still emitted as HTML. If you genuinely want to exclude a component from the server render entirely, use next/dynamic with { ssr: false } instead.
To apply this to your own codebase:
- Open the file that is emitting the mismatch.
- Identify any call to
Math.random(),Date.now(), or directwindowusage inside the render body. - If the component does not need to be server‑rendered, prepend
'use client';and replace the nondeterministic call with a stable API (useId,useEffect‑based state, etc.). - Save and restart the dev server (
npm run dev) if it was already running.
When the value genuinely must be random: useEffect + state#
If the component must stay server‑rendered but you truly need a nondeterministic value, generate it after mount so the first render is stable on both sides:
import React, { useEffect, useState } from 'react';
export default function RandomBadge() {
const [id, setId] = useState('');
useEffect(() => {
setId(Math.random().toString(36).substring(2, 9));
}, []);
return (
<span id={id ? `badge-${id}` : undefined} className="badge">
{id ? `Random ID: ${id}` : 'Loading…'}
</span>
);
}The same pattern handles timestamps. A component calling new Date().toLocaleString() directly in the render body becomes:
import React, { useEffect, useState } from 'react';
export default function TimeStamp() {
const [now, setNow] = useState('');
useEffect(() => {
setNow(new Date().toLocaleString());
}, []);
return <p>{now || 'Loading time…'}</p>;
}Third-party libraries that mint their own IDs#
Some UI libraries generate IDs on mount. Wrap the component with 'use client' or use the library's SSR‑compatible API. For example, Material‑UI's TextField accepts an id prop; generate it with useId instead of letting the library auto‑generate.
Checking the server-rendered HTML directly#
Start the dev server:
npm run dev> next dev
▲ Next.js 14.x.x
- Local: http://localhost:3000
✓ Ready in 2.3sOpen the page that previously threw the error. The browser console must no longer contain the hydration warning. Instead you'll see the component rendered with a stable ID or, if you used 'use client', the element appears after the initial paint without any warning.
To double‑check, inspect the server‑rendered HTML directly (the /_next/data/ JSON endpoint is a Pages Router feature and is not available in the App Router):
curl -s http://localhost:3000/ | grep 'badge-'If the server is emitting the component HTML, you will see the stable badge-r:1 (or similar useId value) in the curl output, confirming deterministic server output.
Guardrails: ESLint rules and dual-render tests#
React's hydration algorithm is intentionally strict because any mismatch can lead to subtle bugs where the client and server diverge in state. The rule of thumb is never put side‑effects or nondeterministic values directly in the render function. In a Next.js project you can enforce this with a couple of safeguards:
- ESLint rule – add
eslint-plugin-react-hooksand enablereact-hooks/exhaustive-depsplus a custom rule that flagsMath.random,Date.now, andnew Dateinside JSX. Example config:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn",
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='Math'][callee.property.name='random']",
"message": "Math.random() in render causes hydration mismatches. Move it to useEffect or use useId."
},
{
"selector": "CallExpression[callee.object.name='Date'][callee.property.name='now']",
"message": "Date.now() in render causes hydration mismatches. Move it to useEffect."
},
{
"selector": "NewExpression[callee.name='Date']",
"message": "new Date() in render causes hydration mismatches. Move it to useEffect."
}
]
}
}-
Unit test – render a component with
@testing-library/reactboth on the server (renderToString) and client (render) and assert that the HTML strings match. A failing test will surface the mismatch before it reaches production. -
Component hygiene – whenever you need a unique identifier, reach for
useId(React 18+) or a deterministic hash of stable props. If you must use a random value, generate it insideuseEffectand store it in state.
By baking these practices into your CI pipeline, you'll catch hydration mismatches early and keep your Next.js app fast and reliable.
Related#
- Next.js Hydration Mismatch Error: Exact Fixes for App Router and React
- React Server Components: Complete Deep Dive
- TypeError cookies() crash in Next.js route handler
- Window is not defined in Next.js – 2026 Fix for React Apps
- Fix useEffect Running Twice in React 18 — Strict Mode
- Next.js useSearchParams Suspense: Static Rendering Fix 2026
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Fix 'Hydration failed' in Next.js: 8 Root Causes (2026)
Hydration mismatch errors breaking your Next.js app? Learn the root causes and 8 proven fixes to eliminate these errors permanently.
Fix "lcp" not working in production
When LCP data never appears in your analytics, a missing reportWebVitals export is usually to blame. Follow these steps to fix it.
TypeError cookies() crash in Next.js route handler
A production‑grade fix for the `TypeError: cookies() is not a function` crash that appears in Next.js route handlers after a deploy.
Browse by Topic
Find stories that matter to you.
