Suggest an editImprove this articleRefine the answer for “What is useId hook in React?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**useId** is a React 18 hook that generates a stable, unique ID consistent across server and client rendering. Use it to link form labels to inputs (`htmlFor`/`id`) or ARIA attributes without SSR hydration mismatches. ```tsx const id = useId(); // ":r0:" - same on server and client <label htmlFor={id}>Email</label> <input id={id} /> ``` **Key:** unlike `Math.random()`, the ID stays identical through SSR hydration.Shown above the full answer for quick recall.Answer (EN)Image**useId** is a React 18 hook that generates a stable, unique ID guaranteed to match between server and client rendering. ## Theory ### TL;DR - `useId` produces IDs like `:r0:`, `:r1:` using React's internal counter, not randomness - Main difference from `Math.random()`: server and client always produce the exact same value - Use it for `htmlFor`/`id` pairs and `aria-*` attributes in SSR apps; skip it for pure client-side code - Never call it inside `.map()` - call once per component, then append stable data ### Quick example ```tsx import { useId } from 'react'; function EmailInput() { const id = useId(); // Same ":r0:" on server and client return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> ); } ``` One call, one stable ID. Server renders `:r0:`, browser hydrates with `:r0:`. No mismatch warning. ### Why random IDs break SSR When React renders on the server, it outputs HTML. The browser then receives that HTML and React hydrates it - attaching event listeners and reconciling the virtual DOM. If any attribute value differs between server HTML and what React generates client-side, you get a hydration mismatch warning. `Math.random()` gives `"0.123..."` on the server and `"0.456..."` on the client. Different values break the `htmlFor`/`id` link. Screen readers lose the label-to-input association. `useId` avoids this because React's counter runs the same sequence on both sides. ### How it works React keeps a global counter per component tree root. During SSR it increments that counter as it renders, then serializes the state. On the client, `hydrateRoot` starts from the same index and replays - so every `useId` call lands on the same number it did on the server. The colon wrapping (`:r1:`) is intentional. Colons have special meaning in CSS (pseudo-classes, pseudo-elements), so an ID like `:r0:` cannot be accidentally targeted by a stylesheet. These IDs are for accessibility linking, not CSS. ### When to use - `htmlFor`/`id` pairs in forms with SSR - `aria-labelledby` and `aria-describedby` in accessible components (tooltips, modals, descriptions) - Multiple IDs in one component - call `useId()` once, then append suffixes: `${id}-email`, `${id}-password` Skip it for pure client-side apps. Any unique string works there. Skip it for `data-testid` too - just hardcode `"email-input"` in tests. Using `useId` for test attributes adds React allocator overhead with no benefit. ### Common mistakes **Calling useId inside .map()** ```tsx // ❌ New allocator slot on every render - IDs change {users.map(u => <input key={u.id} id={useId()} />)} // ✅ Call once per component, append stable data function UserInput({ user }) { const baseId = useId(); return <input id={`${baseId}-${user.id}`} />; } ``` Each `useId()` call inside a loop opens a new slot on every render. Server and client counters fall out of sync. **Using it for list keys** ```tsx // ❌ Wrong {items.map(item => <li key={useId()}>...</li>)} // ✅ Use data from the item itself {items.map(item => <li key={item.id}>...</li>)} ``` Keys must be stable across renders. `useId` inside a map produces different IDs each time. **Assuming global uniqueness across multiple roots** IDs are scoped per root. Portals and separate `ReactDOM.createRoot` calls each have their own counter, so `:r1:` can appear in more than one tree. If they share the DOM, add a manual prefix: `useId() + '-modal'`. ### Real-world usage - Material-UI v6+: input `aria-describedby` links generated internally with `useId` - React Aria (Adobe): all form label associations use `useId` - Chakra UI: modal `aria-labelledby` relies on it - Next.js App Router: server components get stable form field IDs without extra config ### Follow-up questions **Q:** Why does the ID use colons like `:r0:`? **A:** Colons avoid CSS conflicts. An ID starting with `:` cannot be selected with a plain CSS rule, which is intentional - these IDs exist for accessibility, not styling. **Q:** What happens with multiple roots or portals? **A:** Each root has its own counter. If two roots share the DOM, their IDs can overlap. Add a manual prefix to prevent collisions. **Q:** Does React StrictMode break useId? **A:** No. StrictMode double-invokes effects in development but preserves the ID counter for hydration. Production SSR is unaffected. **Q:** Can I seed or customize the generated IDs? **A:** No public API exists for that. If you need readable IDs in tests, use hardcoded `data-testid` values instead. **Q:** (Senior) How does useId interact with Suspense boundaries in streaming SSR? **A:** Each Suspense boundary tracks the ID counter independently. When a lazy subtree streams in, it picks up the correct sequence. Mismatches happen only if you violate hook call order inside a boundary. ## Examples ### Login form with label-input pairs ```tsx import { useId } from 'react'; function LoginForm() { const id = useId(); // One base ID for the whole form return ( <form> <label htmlFor={`${id}-email`}>Email</label> <input id={`${id}-email`} type="email" /> <label htmlFor={`${id}-password`}>Password</label> <input id={`${id}-password`} type="password" /> </form> ); } ``` One `useId` call gives a base like `:r3:`. Both fields get stable IDs (`:r3:-email`, `:r3:-password`) without a second hook call. In my experience, this suffix pattern is cleaner than calling `useId` twice - fewer allocator slots, same result. ### Accessible tooltip with aria-describedby ```tsx import { useId } from 'react'; function Tooltip({ text, children }: { text: string; children: React.ReactNode }) { const id = useId(); return ( <div> <div aria-describedby={id}>{children}</div> <div role="tooltip" id={id}>{text}</div> </div> ); } ``` `aria-describedby` on the trigger and `id` on the tooltip need to match for assistive technology to read the description. On a server-rendered page, `useId` guarantees that match without any manual coordination.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.