Hydration Proof

Search documentation

Find a page or section

Fix the Math.random hydration error

Every render rolls the dice again, and hydration is a render.

A Math.random hydration error happens when render code generates a value twice: once on the server and again in the browser. The results differ. Generate the value on the server and pass it down, use useId for element ids, or wait until after hydration to generate it.

Symptoms

Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "Lucky number 0.72810394" Client: "Lucky number 0.19420517"
Minified React error #418; visit https://react.dev/errors/418

A crypto.randomUUID hydration mismatch works the same way. If the value appears in an attribute, React 19 may leave the server attribute in place without a production warning.

hydration-proof reports changed text as HP1001 and attributes as HP1002. It looks for random-looking values and can test the cause with a probe.

Why the Math.random hydration error happens

These calls can produce a different result on the first browser render:

  • Math.random()
  • crypto.randomUUID() and crypto.getRandomValues(), and randomUUID, randomBytes and randomInt from node:crypto
  • v1, v4, v6 and v7 from uuid (v3 and v5 are deterministic)
  • nanoid(), and generators made with customAlphabet() or customRandom()
  • uniqueId, random and sample from lodash

Moving the call into a state initializer does not help. The initializer can run on both sides too.

How to fix it

Pick the value on the server and pass it down

In Next.js, make the choice in a Server Component and pass it as a prop:

app/tips/page.tsx
import { Tip } from "./tip";
 
// A Server Component: the choice is made once, on the server.
export default async function Page() {
  const tips = await getTips();
  const tip = tips[Math.floor(Math.random() * tips.length)];
  return <Tip tip={tip} />;
}
app/tips/tip.tsx
"use client";
 
export function Tip({ tip }: { tip: string }) {
  return <p>{tip}</p>;
}

For many values, you can send a seed and use the same deterministic generator on both sides.

Use useId for element ids

For accessibility ids, use React's useId. It produces matching ids for the server and first browser render:

email-field.tsx
import { useId } from "react";
 
export function EmailField() {
  // Before: const id = `email-${Math.random()}`;
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} type="email" />
    </>
  );
}

Generated id mismatches covers counters, several React roots and component libraries.

Randomize after hydration

If nothing needs the random value before the page is interactive, start from a stable value and choose a new one in an effect:

shuffle-tip.tsx
"use client";
 
import { useEffect, useState } from "react";
 
// A Math.random hydration error, Next.js or any SSR app: never roll the dice in render.
export function ShuffleTip({ tips }: { tips: string[] }) {
  const [tip, setTip] = useState(tips[0]);
 
  useEffect(() => {
    setTip(tips[Math.floor(Math.random() * tips.length)]);
  }, [tips]);
 
  return <p>{tip}</p>;
}

Generating a value in onClick or onSubmit is also safe because those handlers run after hydration.

Catch it with ESLint

no-random-in-render catches random calls during render. no-unstable-id gives a more specific warning when the value becomes an element id.

npm install -D eslint-plugin-hydration-proof

Catch it in CI

With --probe, hydration-proof changes the browser's random seed and checks whether the finding changes too. The server is not changed. See probes.

npx hydration-proof test --probe