skip to content

useSyncExternalStore and useId

You will learn the hook every state library subscribes through — subscribe plus getSnapshot, with a server snapshot for SSR — and why it exists to prevent tearing under concurrent rendering. useId comes along as the small but frequently asked hook for generating ids that match between server and client markup.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

What problem does React's useId hook solve, and why are a module-level counter or Math.random() bad substitutes for it?

level: juniorimportance: should knowfreq 50%

answer

  1. ids must survive hydration
  2. server and client render separately
  3. randomness differs between the two passes
  4. derived from position in the tree
  5. label and aria wiring is the use case

basics

~20 s

useId generates a unique string id that is identical on the server and on the client, so attributes like htmlFor and aria-describedby survive hydration. A random value differs between the two renders, and a module counter depends on render order, so both can produce mismatched markup.

solid answer

~50 s

`useId` returns a unique string that React derives from the component's position in the tree, which makes it stable across server rendering and client hydration. That is the whole point: you need an id to wire a `<label htmlFor>` to an input, or an `aria-describedby` to its description, and if the server writes one id into the HTML while the client's first render computes a different one, hydration sees mismatched attributes and the accessibility wiring breaks. `Math.random()` or `crypto.randomUUID()` produce a different value on each render pass by construction, so they mismatch every time. A module-level counter is worse than it looks: it depends on how many components happened to render before this one, which differs between a server request and a fresh client hydration, and it does not reset between requests on a long-lived server. `useId` is also correct across multiple instances of the same component, since each instance occupies a different tree position.

code

javascript · 11 lines
javascript
import { useId } from 'react';

export function PasswordField() {
  const hintId = useId();
  return (
    <div>
      <input type="password" aria-describedby={hintId} />
      <p id={hintId}>Use at least 12 characters.</p>
    </div>
  );
}

go deeper

for a junior

Be ready to say useId gives you a unique id string that is the same on the server and the client, and that you use it to connect a label or an aria attribute to an input.

for a middle

Explain the hydration constraint concretely: the HTML and the first client render must agree, so React derives the id from tree position instead of generating randomness.

for a senior

Show why the common workarounds fail on their own terms — random values differ per pass, module counters depend on render order and leak across server requests — and treat the id as an opaque attribute value.

for a principal

Set the team convention: ids for accessibility wiring come from this hook, data identity comes from the domain, and multi-root pages namespace themselves with identifierPrefix rather than inventing per-app schemes.

## The requirement Accessible markup depends on ids that connect elements: ```jsx <label htmlFor={id}>Email</label> <input id={id} aria-describedby={hintId} /> <p id={hintId}>We never share it.</p> ``` The ids must be unique on the page — two inputs with the same id break both the label association and any assistive technology that follows it — and, in a server-rendered app, they must be *the same string* in the HTML the server produced and in the first render the client performs while hydrating. Those two requirements together are what makes id generation harder than it looks. ## Why hydration is the hard constraint A server-rendered page is produced twice: once as HTML on the server, and once as a client render that attaches React to that HTML. Hydration assumes the two agree. If the server wrote `for="a1"` and the client's first render computes `for="b7"`, the attribute the browser sees no longer matches what React thinks it rendered, and the association is wrong even when the page looks fine. `useId` solves this by not generating randomness at all. React derives the id from where the component sits in the tree, and since the server and the client render the same tree, they derive the same string. Call the hook in a component and every instance gets its own value, because each instance has its own position. ```jsx import { useId } from 'react'; function EmailField() { const id = useId(); return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> ); } ``` Render `<EmailField />` three times and you get three distinct, hydration-stable ids with no bookkeeping. ## Why the usual substitutes fail **`Math.random()` / `crypto.randomUUID()` / `Date.now()`.** These are non-deterministic by design. The server produces one value, the client's hydration render produces another, and the markup mismatches on every single render. Some codebases hide this by generating the id in a `useState` initializer or a ref — that stabilises it *within* a client session but still differs from the server's value, so the hydration problem is untouched. **A module-level counter** (`let n = 0; const id = 'field-' + n++`). This looks deterministic and is not, for two reasons. It depends on how many other components incremented it first, which is a function of render order and can differ between the server pass and the client pass. And on a long-lived server the module is shared across requests, so the counter keeps climbing and two users get different ids for the same component — including ids that no client render will ever reproduce. **A hard-coded string** (`id="email"`). Deterministic, hydration-safe, and broken as soon as the component is rendered twice on one page: duplicate ids on a document are invalid and the label associates with whichever element the browser resolves first. **A prop passed down from the parent** is a legitimate option — the parent chooses the id and passes it in — but it pushes uniqueness bookkeeping onto every call site, which is exactly the chore `useId` removes. ## Properties of the returned value - It is a **string**, suitable for use directly in `id`, `htmlFor`, `aria-describedby`, `aria-labelledby` and similar attributes. - It is **stable across renders** of the same component instance, so it does not change when state updates. - Its **format is opaque**. React does not guarantee the shape of the string, and it has changed between React versions, so never parse it, never assume it is a valid bare CSS selector, and never construct one by string-concatenating it into a stylesheet. Use it as an attribute value. - It is **not a data identity**. It identifies a rendered position, not a record, so it is the wrong thing to persist, send to a server, or use as a React `key`. ## Multiple roots on one page When two independent React roots render into the same document, their generated ids could in principle collide. React exposes an `identifierPrefix` option on the root creation APIs (`createRoot` and `hydrateRoot`, and the server rendering APIs) so each root can namespace its ids. This is a real answer to a real problem, but it is worth mentioning only when the interviewer raises multi-root or micro-frontend setups — it is not part of everyday usage. ## How to answer Lead with the hydration requirement, since that is why the hook exists rather than a utility function. Then name the accessibility use case it serves, and dismiss random and counter-based approaches with the specific reason each fails: non-determinism for one, render-order and cross-request dependence for the other.

  • Why is generating the id once in a useState initializer not good enough for a server-rendered app?
    It stabilises the id across re-renders on the client, which is the easy half. The server still produced a different value when it rendered the HTML, so the first client render disagrees with the markup it is hydrating. Determinism across renders is not the same as agreement between server and client.
  • Can you use the value returned by useId as the key for an item in a list?
    No. Keys must identify data so React can match items across reorders and updates, and useId identifies a rendered position instead — it stays the same when the data at that position changes. Keys come from stable properties of your items, such as a record id.
  • Two independent React roots render on the same page. How do you keep their generated ids from colliding?
    Give each root a distinct `identifierPrefix` when you create it. `createRoot` and `hydrateRoot` accept it as an option, and the server rendering APIs accept it too, so each root's ids live in their own namespace. It mainly matters for micro-frontends and embedded widgets.

saying these in an interview costs you the question

  • Says Math.random() is fine because it runs once
  • Uses useId values as list keys
  • Hard-codes one id and reuses the component twice
  • Thinks useId identifies data rather than tree position
  • Parses or slices the returned id string

context

open as a page

When is the third argument to React's useSyncExternalStore, getServerSnapshot, required, what does it have to return, and what happens if you leave it out?

level: middleimportance: should knowfreq 30%

basics

~20 s

getServerSnapshot is required whenever the component is server-rendered. React calls it on the server and again for the initial hydration render, so it must return a value available without browser APIs and identical on both sides. Omitting it makes React throw during server rendering.

open as a page

In React, what three arguments does useSyncExternalStore take, what is each one required to do, and what does the hook return?

level: middleimportance: should knowfreq 45%

basics

~20 s

useSyncExternalStore takes subscribe (registers React's change callback and returns an unsubscribe function), getSnapshot (reads the store's current value), and an optional getServerSnapshot used during server rendering. It returns the current snapshot and re-renders the component when that snapshot changes.

open as a page

A React component subscribes to an external store with useSyncExternalStore, and its getSnapshot is written as `() => ({ items: store.items, total: store.items.length })`. The page freezes and React warns that the result of getSnapshot should be cached. What is going wrong, and how do you fix it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

getSnapshot builds a new object on every call, so React's Object.is comparison of consecutive snapshots is never equal. React re-renders, reads again, sees another new object, and loops. Fix it by caching the snapshot object in the store and recomputing it only when the store is written.

open as a page

A React form component renders three labelled inputs that each need their own id. Do you call useId three times, and how do you derive the ids?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Call useId once and append a suffix per field, such as ${id}-email and ${id}-name. Three separate calls also work, but one call plus suffixes scales to any number of related attributes and keeps the shared prefix visible in the markup.

open as a page