Hardcoded IDs break the moment a component renders more than once on the same page. useId generates a unique, stable identifier per component instance that matches perfectly between server rendering and client hydration. This lesson covers how and when to use it correctly.
1The Problem with Hardcoded IDs
Form controls need a unique id to correctly associate a label with its input for accessibility. A hardcoded id works only as long as the component renders exactly once — the moment it renders twice on the same page, both instances share the same id, breaking both accessibility semantics and DOM lookup uniqueness.
2useId Generates a Unique ID Per Component Instance
useId returns a string that's unique and stable for the lifetime of a specific component instance, guaranteed not to collide with any other useId-generated value in the app, even across multiple instances of the same component rendered side by side.
3One useId, Multiple Related IDs
When a component needs several related identifiers — such as an input's id and an error message's id linked via aria-describedby — the recommended pattern is calling useId once and deriving suffixed variants from that single base value, rather than calling useId multiple times.
4Not for List Keys
useId is explicitly not designed to generate keys for a rendered list. List keys should be derived from the underlying data — a database ID or another stable unique field — so React can correctly track additions, removals, and reordering; useId produces one identifier per component instance, not per array item.
5Why useId Exists: Server/Client ID Matching
A naive random-ID approach like Math.random() produces a different value during server rendering than during client hydration, triggering a hydration mismatch. useId is purpose-built to generate the exact same identifier on both the server-rendered HTML and the client's hydration pass, avoiding that entire class of bug.
6Step-by-Step Breakdown
The Problem with Hardcoded IDs. Form inputs need a unique id to link a <label> to its <input> for accessibility. Hardcoding id="email" works — until that component renders twice on the same page, and suddenly two inputs share the same id, breaking both accessibility and any document.getElementById lookup relying on uniqueness.
useId Generates a Unique ID Per Component Instance. useId returns a unique string, stable for the lifetime of that component instance, guaranteed not to collide with the ID from any other useId call anywhere in the app — even across two instances of the exact same component rendered side by side.
Why is useId() safer than hardcoding id="email" inside a reusable component?
- →It guarantees a unique ID even if the component renders multiple times
- →It always produces a shorter string
One useId, Multiple Related IDs. When a single component needs several related IDs — like a field's input, its error message, and its description, linked via aria-describedby — call useId once and derive suffixed variants from it, rather than calling useId multiple times for the same logical group.
Not for List Keys. useId is explicitly not meant to generate keys for a rendered list. Keys should come from your data (a database ID, a stable unique field) so React can correctly track which items were added, removed, or reordered. useId produces one ID per component instance, not per array item.
True or False: useId is the recommended way to generate the key prop for a list rendered with .map().
- →True
- →False
Why useId Exists: Server/Client ID Matching. A naive random ID generator like Math.random() produces a different value on the server than during client hydration, causing a hydration mismatch error. useId is specifically designed to produce the exact same ID on both the server-rendered HTML and the client's hydration pass, avoiding that class of bug entirely.
Mastery Achieved. You now know when and why to use useId: generating unique, collision-free IDs for accessibility attributes like htmlFor and aria-describedby, deriving multiple related IDs from a single call, avoiding it for list keys, and understanding why it matches perfectly between server and client. Next, you'll learn useLayoutEffect for DOM measurements that must happen before paint.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported since React 18.
Fully supported since React 18.
Fully supported since React 18.
Fully supported since React 18.
Accessibility (A11y)
1useId Is Primarily an Accessibility Tool
Its main practical use is generating reliable values for htmlFor, id, aria-describedby, and aria-labelledby, ensuring label-input associations and descriptive relationships remain correct even when a component is reused multiple times on the same page.
SEO Implications
- 1
useId Prevents SSR Hydration Errors That Can Break Rendering
A hydration mismatch can cause React to discard and re-render server HTML on the client, momentarily showing broken or flashing content — useId avoiding this class of bug helps keep server-rendered content stable and reliable for both users and crawlers.
Best Practices
Call useId Once Per Component, Derive the Rest
For a component needing multiple related IDs, call useId a single time and build suffixed strings from it, rather than calling the hook repeatedly for each identifier.
Never Use useId for React's key Prop
Keys must be derived from stable, data-identifying values so React can track list identity across reorders — useId identifies component instances, not list items, and using it for keys defeats React's reconciliation.
Frequent Bugs
Two instances of the same reusable form field component on one page cause a screen reader to announce the wrong label for an input.
The component used a hardcoded id instead of useId, so both instances share the same id and htmlFor value, breaking the label association for one of them. Replace the hardcoded id with a useId()-generated one.
A server-rendered app shows a hydration mismatch warning tied to an id attribute.
A random ID generator like Math.random() or a counter reset per render was used instead of useId, producing different values between the server render and client hydration. Replace it with useId, which is specifically built to match between server and client.
Real-World Examples
A Reusable Accessible Text Field Component
A design system's TextField component needs to correctly associate its label, input, and error message via id, htmlFor, and aria-describedby, and it's used dozens of times across a single form. Using a single useId() call per instance, with suffixed derived IDs, keeps every instance's accessibility wiring correct regardless of how many times the component renders.
function TextField({ label, error }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} aria-describedby={error ? `${id}-error` : undefined} />
{error && <span id={`${id}-error`}>{error}</span>}
</div>
);
}