# Fix the Math.random hydration error

> Why a Math.random hydration error happens when render code calls Math.random(), crypto.randomUUID() or uuid, and how to pick the value once or use useId.

Source: https://hydration.jscrate.dev/docs/causes/random
Last updated: 2026-09-18

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

```text
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, 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](https://hydration.jscrate.dev/docs/issues/hp1001) and attributes as
[HP1002](https://hydration.jscrate.dev/docs/issues/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()` 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

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:

```tsx title="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} />;
}
```

```tsx title="app/tips/tip.tsx"
"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:

```tsx title="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](https://hydration.jscrate.dev/docs/causes/unstable-id) 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:

```tsx title="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>;
}
```

Event handlers run after hydration too, so a `crypto.randomUUID()` inside
`onClick` or `onSubmit` is fine.

## Catch it with ESLint

[`no-random-in-render`](https://hydration.jscrate.dev/docs/rules/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`](https://hydration.jscrate.dev/docs/rules/no-unstable-id) instead, with a message that
points at `useId`.

```bash
npm install -D eslint-plugin-hydration-proof
```

## Catch 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](https://hydration.jscrate.dev/docs/probes).

```bash
npx hydration-proof test --probe
```

## Related

- [Generated ids that differ](https://hydration.jscrate.dev/docs/causes/unstable-id)
- [Date.now() and other time values](https://hydration.jscrate.dev/docs/causes/time)
- [The no-random-in-render rule](https://hydration.jscrate.dev/docs/rules/no-random-in-render)
- [HP1002: attribute differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1002)
- [All causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
