Hydration Proof

Search documentation

Find a page or section

Disallow random values while a component renders.

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().

Rulehydration-proof/no-random-in-render
What it reportsDisallow random values while a component renders
recommended / nextError
strictError
Server ComponentsSkipped with the next preset (they never hydrate)
SuggestionsNo
Optionsnone

What it reports

These calls in render code:

  • Math.random()
  • crypto.randomUUID() and crypto.getRandomValues() (the global, window.crypto, or node:crypto's randomUUID, randomBytes, randomInt)
  • v1, v4, v6 and v7 imported from uuid (named or namespace imports; v3 and v5 are deterministic and allowed)
  • nanoid() from nanoid (and nanoid/non-secure), and functions created at module level with customAlphabet() or customRandom()
  • uniqueId, random and sample from lodash, lodash-es, lodash/<name> or lodash.<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:

app/tips/page.jsx
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.

  • no-unstable-id reports random values that end up in ids. Those calls are not reported by this rule.
  • require-deterministic-list-order reports random sort comparators and lodash's shuffle/sampleSize. Those are not reported by this rule.
  • eslint-plugin-react-hooks: its purity rule (part of the React Compiler rules) also flags known impure calls such as Math.random() during render. This rule explains the hydration consequence, skips Server Components, and also covers crypto, uuid, nanoid and lodash helpers. With both enabled you see two reports for Math.random(); turn one off if you prefer a single report.
  • Random values and hydration: every fix for this cause