Math.random() in render returns one value on the server and another when the
browser hydrates, so the HTML and the first client render disagree.
no-random-in-render reports Math.random(), crypto.randomUUID(), uuid,
nanoid and lodash's random helpers in render code, and points you to a value
the server creates or useId().
| Rule | hydration-proof/no-random-in-render |
|---|---|
| What it reports | Disallow random values while a component renders |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset (they never hydrate) |
| Suggestions | No |
| Options | none |
What it reports
These calls in render code:
Math.random()crypto.randomUUID()andcrypto.getRandomValues()(the global,window.crypto, ornode:crypto'srandomUUID,randomBytes,randomInt)v1,v4,v6andv7imported fromuuid(named or namespace imports;v3andv5are deterministic and allowed)nanoid()fromnanoid(andnanoid/non-secure), and functions created at module level withcustomAlphabet()orcustomRandom()uniqueId,randomandsamplefromlodash,lodash-es,lodash/<name>orlodash.<name>
Only imports are matched, so a local function called nanoid is not
reported.
Why Math.random() in render breaks hydration
Every render produces a new value, and the hydration render is a new render:
server HTML: <div class="card card-0.7281">
client render: <div class="card card-0.1942">Text differences are reported by React and make it render the page again.
Attribute differences (className, style, data-*) are worse: React 19
keeps the server value without reporting it in production, so the page
silently runs with the wrong attribute. See
attributes didn't match
for that case.
Incorrect
import { v4 as uuid } from "uuid";
import { sample } from "lodash";
function Tip({ tips }) {
return <p>{sample(tips)}</p>;
}
function Card() {
const [seed] = useState(() => Math.random());
return <div data-seed={seed} />;
}
function Upload() {
const key = uuid();
return <Dropzone key={key} />;
}Correct
Pick on the server and pass the result down. With the next preset, this
file is a Server Component and is not checked:
export default async function Page() {
const tips = await getTips();
return <Tip tip={tips[Math.floor(Math.random() * tips.length)]} />;
}Or render something stable first and randomize after hydration:
function Tip({ tips }) {
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.
function Upload() {
const onDrop = (files) => save(files, crypto.randomUUID());
return <Dropzone onDrop={onDrop} />;
}For ids, use useId() (see no-unstable-id).
Options
This rule has no options.
Messages
What ESLint prints for this rule, word for word:
<source>returns a different value on the server and during hydration, so the rendered output does not match. Create the value on the server and pass it down, use useId() for ids, or generate it in useEffect after hydration.
When not to use it
When the component is never server-rendered, or when the random value is only used for something that never reaches the DOM and never changes what is rendered.
Related
no-unstable-idreports random values that end up in ids. Those calls are not reported by this rule.require-deterministic-list-orderreports random sort comparators and lodash'sshuffle/sampleSize. Those are not reported by this rule.- eslint-plugin-react-hooks: its
purityrule (part of the React Compiler rules) also flags known impure calls such asMath.random()during render. This rule explains the hydration consequence, skips Server Components, and also coverscrypto, uuid, nanoid and lodash helpers. With both enabled you see two reports forMath.random(); turn one off if you prefer a single report. - Random values and hydration: every fix for this cause