Fix React 18 Hydration Mismatch in Next.js
Technology

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.

2026-06-02
7 min read
Fix React 18 Hydration Mismatch in Next.js

TL;DR#

If you're seeing React 18: Hydration failed because the initial UI does not match what was rendered on the server, the cause is usually non‑deterministic rendering (random IDs, Date.now(), browser‑only globals) that produces different markup on the server versus the client. Fix it by moving that logic into a client‑only effect or using stable React APIs so the server and client output match exactly.

If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.

What you'll see#

The exact error that lands you on Google looks like 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://reactjs.org/link/hydration-mismatch for more information.

It appears in the browser console right after the first paint when you load a page that uses the Next.js App Router (or Pages Router) with React 18. The mismatch is reproducible on every local dev run, on Vercel preview deployments, and even after a fresh next build. The error is agnostic to environment—it shows up in Chrome, Firefox, and Safari alike—because the underlying cause is purely a markup difference, not a browser quirk.

Root cause#

In React 18 the hydration algorithm expects the HTML string generated by the server to be byte‑for‑byte identical to the markup React will produce on the client during the initial render. When they diverge, React aborts hydration and logs the error above. The divergence is almost always caused by code that runs during render and yields a different result on the server versus the client.

Typical culprits include:

  • 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.

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 any of the above statements run in the function body, the server and client will generate different strings, triggering the hydration error.

The relevant code path in React looks roughly like this (simplified):

js
// react-dom/src/client/hydrateRoot.js
function hydrateRoot(container, element) {
  const serverHTML = container.innerHTML;
  const clientHTML = renderToString(element);
  if (serverHTML !== clientHTML) {
    console.error('Hydration failed because the initial UI does not match...');
  }
}

Because the comparison is literal, even a single millisecond timestamp difference is enough to break hydration.

The fix#

The fix is to ensure that every piece of markup produced during the first render is deterministic. The most common pattern is to move nondeterministic logic into a useEffect (which only runs on the client) or replace it with a stable React API such as useId. Below is a concrete before‑and‑after example that reproduces the error and then resolves it.

Problematic component (causes the error)#

js
// 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.

Fixed component (deterministic)#

js
// components/RandomBadge.tsx
'use client'; // Mark this file as client‑only so it never runs on the server
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: Adding 'use client' tells Next.js to skip server‑side rendering for this component entirely, so the server never emits the <span> at all. The client renders it after hydration, eliminating any comparison. If you still need the component to be part of the server HTML (e.g., for SEO), replace Math.random() with useId, which React guarantees to be consistent between server and client.

Step by step#

  1. Open components/RandomBadge.tsx (or whichever file is emitting the mismatch).
  2. Identify any call to Math.random(), Date.now(), or direct window usage inside the render body.
  3. 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.).
  4. If the component must stay server‑rendered, move the nondeterministic code into a useEffect that sets local state after mount:
js
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>
  );
}
  1. Save the file and restart the dev server (npm run dev) if it was already running.

Verify the fix#

Run the development server:

bash
npm run dev

You should see the usual Next.js startup output:

text
> next dev
ready - started server on http://localhost:3000

Open 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, you can inspect the server‑generated HTML via curl:

bash
curl -s http://localhost:3000/_next/data/development/random-badge.json | jq .

The JSON payload should contain the same id value that the client later displays, confirming deterministic output.

Variant A — Date‑based mismatch#

If your component uses new Date().toLocaleString() directly in the render body, replace it with a client‑only effect:

js
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>;
}

Variant B — Third‑party library 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.

Why this happens (and how to avoid it next time)#

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-hooks and enable react-hooks/exhaustive-deps plus a custom rule that flags Math.random, Date.now, and new Date inside JSX. Example config:
json
{
  "plugins": ["react-hooks", "no-sync-scripts"],
  "rules": {
    "react-hooks/exhaustive-deps": "warn",
    "no-sync-scripts/no-sync-scripts": ["error", { "functions": ["Math.random", "Date.now", "new Date"] }]
  }
}
  • Unit test – render a component with @testing-library/react both 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 inside useEffect and 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.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.