Use useId instead of a random id, a timestamp or a module-level counter: those
values differ between the server render and hydration, so id, htmlFor and
aria-* references stop matching. no-unstable-id reports every value that
changes between renders and ends up in an id, at the call that makes it
unstable.
| Rule | hydration-proof/no-unstable-id |
|---|---|
| What it reports | Disallow ids built from random values, the clock or module-level counters |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset (they never hydrate) |
| Suggestions | No |
| Options | none |
What it reports
Values that change between renders and end up in an id:
- Sources: everything
no-random-in-renderandno-date-in-renderrecognize (Math.random(),crypto.randomUUID(), uuid, nanoid, lodash'suniqueId,Date.now(),new Date(), ...), and module-levellet/varcounters changed during render (nextId++). - Id attributes:
id,htmlFor,for,aria-labelledby,aria-describedby,aria-controls,aria-owns,aria-activedescendant,aria-details,aria-errormessage,listandpopoverTargeton any element;nameoninput,select,textarea,button,fieldsetandoutput; and props ending inIdon components (labelId,triggerId). - Id state:
useStateanduseRefwhose variable is named like an id (id,inputId,idRef).
The value is followed through local variables, useState, useRef and
useMemo initializers, template literals, class fields (this.id) and
module-level constants. The report is on the call (or counter update) that
makes the value unstable.
Why use useId instead of a random id
The id rendered on the server is not the id rendered during hydration:
server HTML: <label for="field-0.4211">Email</label><input id="field-0.4211">
client render: <label for="field-0.8390">Email</label><input id="field-0.8390">React keeps the server's attributes without a warning in production, so the
page works until another render updates some attributes but not others.
Labels then point at nothing and screen readers lose the connection. A
module-level counter is worse: the server keeps counting across requests
(field-5731) while every browser starts at field-1.
useId() produces the same id on the server and during hydration, because it
is derived from the component's position in the tree.
Incorrect
import { nanoid } from "nanoid";
let nextId = 0;
function EmailField() {
const id = `email-${nextId++}`;
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);
}
function Tooltip({ children }) {
const [tooltipId] = useState(() => nanoid());
return <span aria-describedby={tooltipId}>{children}</span>;
}Correct
function EmailField() {
const id = useId();
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);
}
function Tooltip({ children }) {
const tooltipId = useId();
return <span aria-describedby={tooltipId}>{children}</span>;
}For list items, combine useId() with a stable key from your data:
`${id}-${item.id}`.
Options
This rule has no options.
Messages
What ESLint prints for this rule, word for word:
<source>makes<sink>differ between the server render and hydration, so the id and every reference to it (labels, ARIA attributes) do not match. Use React useId() to create ids.
When not to use it
In components that are never server-rendered. Otherwise there is no reason to
turn it off: useId() exists in React 18 and 19.
Related
This rule takes precedence over
no-random-in-render,
no-date-in-render,
no-global-render-counter and
require-deterministic-list-order:
a value reported here is not reported by them.
- Generated ids that differ: the cause and its fixes
- Attributes didn't match, the warning an unstable id produces