A Math.random hydration error happens when a component generates a random value
while it renders. The server rolls one number, the browser rolls another during
hydration, and the HTML no longer matches. Generate the value once on the server
and pass it down, use useId for element ids, or pick the value in an effect
after hydration.
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/418A crypto.randomUUID hydration mismatch works the same way, with a UUID in place
of the decimal. In an attribute (className, style, data-*) the result is
worse: React 19 keeps the server's value in production without reporting it, so
the page silently runs with the wrong attribute.
hydration-proof reports text as HP1001 and attributes as
HP1002, with the cause Random value. It recognizes
random-looking decimals and UUIDs on both sides; in the package's test suite it
names the cause with 94% confidence for Math.random() and 89% for
crypto.randomUUID().
Why the Math.random hydration error happens
Hydration is a render. Anything random that runs during render runs twice, once on the server and once in the browser, and the two results differ:
Math.random()crypto.randomUUID()andcrypto.getRandomValues(), andrandomUUID,randomBytesandrandomIntfromnode:cryptov1,v4,v6andv7fromuuid(v3andv5are deterministic)nanoid(), and generators made withcustomAlphabet()orcustomRandom()uniqueId,randomandsamplefrom lodash
A state initializer does not help: useState(() => Math.random()) runs on the
server and again during hydration.
How to fix it
Pick the value on the server and pass it down
A Server Component renders once, so a random choice there is made once and sent to the client as a prop:
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} />;
}"use client";
export function Tip({ tip }: { tip: string }) {
return <p>{tip}</p>;
}For many random values, send a seed instead and use a seeded generator on both sides: the same seed produces the same sequence.
Use useId for element ids
Random ids for id, htmlFor and aria-* attributes are the most common
source of this bug. React's useId returns the same id on the server and during
hydration:
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
When the value should change on every visit and nothing depends on it before the page is interactive, start from a stable value and randomize in an effect:
"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>;
}Event handlers run after hydration too, so a crypto.randomUUID() inside
onClick or onSubmit is fine.
Catch it with ESLint
no-random-in-render reports every call in
the list above in render code, and skips Server Components with the next
preset. Random values that end up in ids are reported by
no-unstable-id instead, with a message that
points at useId.
npm install -D eslint-plugin-hydration-proofCatch it in CI
hydration-proof test reports each random value as HP1001 or HP1002. With
--probe, it reloads the page with a different seed for Math.random() and
crypto.getRandomValues() in the browser. If the client value changes with
only the seed changed, randomness is the proven cause. The server keeps its
real random values, so fixing the seed never hides the bug. See
probes.
npx hydration-proof test --probe