Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~26 min· topic 4 of 6

Topic 10.4

useId, useDebugValue and Other Small Hooks

In one line

useId makes unique, stable ids that match between server and browser, for connecting labels and ARIA attributes; useDebugValue labels your custom hooks in React DevTools; and React 19.2 has twenty built-in hooks in all, each with a clear job.

Think of it like this

Name tags at a school trip. Two children are both called Aarav, so the teacher writes Aarav-1 and Aarav-2 on their tags. The tags are given out in the same order every trip, so they always match the list on the bus. useId is the teacher handing out name tags to your components: unique on the page, and the same on the server and in the browser.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

useId
A hook that returns a unique id string for this component instance, the same on the server and in the browser.
htmlFor
JSX's name for the for attribute of <label>: it points the label at an input's id.
aria-describedby
An attribute that tells screen readers which element's text describes this one, such as a hint or error.
Hydration mismatch
When the browser's first render doesn't match the HTML the server sent, so React must patch or redo it.
identifierPrefix
A root option that adds a prefix to every useId id, so several React apps on one page can't clash.
useDebugValue
A hook that shows a label for your custom hook in React DevTools. It changes nothing in the app.
React DevTools
The browser extension that shows your component tree, props, state and hooks.

Step by step

01The bug: a hard-coded id in a reusable component

PasswordField hard-codes id="password". Render it twice (password and confirm password) and both inputs share one id: the second label focuses the first input, and the second hint describes the wrong field.

PasswordField.jsxwhole filejsx
function PasswordField({ label }) {
  return (
    <>
      <label htmlFor="password">{label}</label>   {/* BUG when used twice */}
      <input id="password" type="password" aria-describedby="password-hint" />
      <p id="password-hint">At least 8 characters.</p>
    </>
  );
}

02One useId, several related ids

Call useId once per component and add suffixes. Every instance now has its own set of ids.

PasswordField.jsxwhole filejsx
function PasswordField({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id + "-input"}>{label}</label>
      <input id={id + "-input"} type="password" aria-describedby={id + "-hint"} />
      <p id={id + "-hint"}>At least 8 characters.</p>
    </>
  );
}

03Why it matches on the server

The id is derived from where the component sits in the tree (parent, then child index), not from timing or randomness. The server and the browser build the same tree, so they produce the same ids and hydration lines up.

Why it matches on the serverdiagram
Rendering diagram…

04Label a custom hook for DevTools

Inside a custom hook, useDebugValue shows a label next to the hook's name in DevTools. Pass a format function when the label is costly to build.

useOnlineStatus.jswhole filejsx
export function useOnlineStatus() {
  const online = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
  useDebugValue(online ? "Online" : "Offline"); // DevTools: OnlineStatus: "Online"
  return online;
}

// Expensive label? Format lazily:
useDebugValue(lastSeen, (date) => date.toLocaleString());

05Tour every built-in hook

Read the table in the live example. You've already used the hooks up to Phase 10; the rest each get their own topic later. When you see an unfamiliar hook in a codebase, find its row and jump to that topic.

Try it yourself

  1. 1

    See the duplicate-id bug

    In the first example, replace id + "-input" with the fixed string "pw" in both places. Predict: which input gets focus when you click the Confirm password label? Click it and check.

  2. 2

    Inspect with DevTools

    Open the useDebugValue example in a new tab with React DevTools installed, select Cart in the Components panel, and find the label. Then check the console: the format function ran because DevTools asked for it.

  3. 3

    Quiz yourself on the tour

    Cover the "What it does" column and explain each hook in one sentence. Which three would you use in almost every component? Which would you expect only inside libraries? (useInsertionEffect, useSyncExternalStore, useDebugValue.)

Code & diagrams

useId for labels and hints React 18+ New tab

Two instances, two separate sets of ids. Click each label: it focuses its own input. The exact format (`_r_0_` in React 19.2) is an internal detail.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Password -> _r_0_-input described by _r_0_-hint
Confirm password -> _r_1_-input described by _r_1_-hint
Same ids on the server, prefixes for several roots React 18+ New tab

Server ids (capital `R`) are built from the tree path so hydration can reproduce them; ids made in a client-only root (small `r`) come from a counter shared by the whole page, which is why the chat root continues at 2. Never rely on either format.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

<p><label for="_R_1_">Name</label> <input id="_R_1_"/></p><p><label for="_R_2_">City</label> <input id="_R_2_"/></p>
_shop-r_0_ _shop-r_1_ _chat-r_2_ _chat-r_3_
useDebugValue: a label only DevTools reads New tab

Without DevTools open, the format function never runs. With React DevTools, select `Cart` and the hooks panel shows `Cart: "2 items, total 270"` next to the hook's name.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Cart rendered with 2 items
A tour of every built-in hook New tab

The console proves the table is complete for React 19.2: every `use...` export of `react` is listed, plus `useFormStatus` from `react-dom`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

hooks exported by react: 19
missing from the table: []
useFormStatus in react-dom: function

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use Math.random() for ids in a server-rendered app

Replace const id = useId(); with const id = "f" + Math.random().toString(36).slice(2); in a component that is server-rendered and then hydrated with hydrateRoot.

terminal
$ Console
── what you'll see ──
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
 
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
...

Break #2

Call useId inside map for each option

In a radio group, write options.map((o) => { const id = useId(); return <span key={o}><input id={id} ... /><label htmlFor={id}>{o}</label></span>; }).

terminal
$ npx eslint src/
── what you'll see ──
6:20 error React Hook "useId" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks

Myth vs fact

Myth

useId is a general unique-id generator.

Fact

It's for ids that connect elements in the DOM (labels, ARIA). For data ids, generate them when the data is created; for keys, use the data's id.

Myth

useDebugValue shows up in the console.

Fact

It only appears in React DevTools' Components panel, next to the custom hook that called it.

Myth

You need to learn every hook before building apps.

Fact

useState, useEffect, useRef, useContext and custom hooks cover most components. The rest solve specific problems; the tour tells you where to find each when you need it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Server-rendered ids (capital R) are derived from the tree path; a client-only root uses a global counter (lower-case r). During hydration the client follows the server's scheme, which is why the ids match. The format has changed across React versions (React 18 used :r0:), which is one reason not to depend on it.

  • ▸

    React 19.2's _r_0_ format contains only characters that are valid in CSS identifiers, so document.querySelector("#" + id) works without CSS.escape. Still prefer refs over querying by id.

  • ▸

    useId is stable for the life of the component, so it's also a handy stable token for things like a name for a radio group inside a reusable component (name={id}), where every instance needs its own group.

  • ▸

    useDebugValue's format function receives the first argument and runs lazily, so it can do expensive work (serialising a store) at zero cost in normal use. React DevTools also lets you see each built-in hook's value per custom hook, so useDebugValue is mostly for summarising.

Remember this

  1. 1

    Why ids matter. HTML connects elements by id: <label htmlFor="email"> points at <input id="email">, and aria-describedby="email-hint" tells a screen reader which text describes the input (see the /html course on forms and accessibility). Hard-coding id="email" breaks as soon as a component appears twice on a page: two elements share an id, and clicking the second label focuses the first input. You need an id that is unique per component instance.

  2. 2

    **useId()** returns a string like _r_0_ that is unique within the React app and stays the same for that component across renders. Call it once and build related ids from it: id + "-input", id + "-hint". It takes no arguments and is a hook, so call it at the top level. Don't use it for list keys (keys should come from your data, Topic 3.3) and don't parse or depend on its format, which has changed between React versions.

  3. 3

    **Why not a counter or Math.random()? A random id differs between the server's HTML and the browser's first render, which is a hydration mismatch** (Topic 20.6). A global counter depends on the order components happen to render, which can differ between server and client (lazy components, Suspense, streaming). useId builds the id from the component's position in the tree, which is the same on both sides. That's its whole reason to exist. If one page has several React roots, give each a prefix with createRoot(el, { identifierPrefix: "shop-" }) (and the same option on the server) so their ids can't collide.

  4. 4

    **useDebugValue(value, format?)** adds a label next to your custom hook in React DevTools' Components panel. Call it inside a custom hook: useDebugValue(isOnline ? "Online" : "Offline"). It does nothing visible in the app and costs almost nothing. If building the label is expensive (formatting a date, summarising a big object), pass a format function as the second argument: React calls it only when DevTools actually inspects the component. Use it in shared library hooks; for small app hooks it's rarely worth it.

  5. 5

    The full set. React 19.2 exports **19 hooks from react** and one from react-dom (useFormStatus). Grouped by job: state (useState, useReducer), context (useContext), refs (useRef, useImperativeHandle), effects (useEffect, useLayoutEffect, useInsertionEffect, useEffectEvent), performance (useMemo, useCallback, useTransition, useDeferredValue), external data (useSyncExternalStore, use), forms and actions (useActionState, useOptimistic, useFormStatus), and helpers (useId, useDebugValue). The live table below lists each one with the topic where it is taught, and checks the list against the real react package.

  6. 6

    Two names you may meet in old code or search results: useFormState from react-dom is the old name of useActionState (renamed in React 19; it still works, but the development build warns ReactDOM.useFormState has been renamed to React.useActionState.), and functions named unstable_... are experimental and not for apps. use is the odd one out: it's a hook-like API that can be called inside if statements and loops (Topic 14.4).

Explain it without notes

01

Why can't you hard-code ids in a reusable component, and how does useId solve it?

02

Why does useId exist instead of using a counter or Math.random()?

03

What does useDebugValue do, and when should you pass a format function?

04

Name the hooks React 19.2 provides for effects and say how they differ.

Practice

01

Write a reusable Checkbox component with a label and a hint text, connected with useId, and render it three times.

02

Make a RadioGroup component that renders three options; every instance must have its own name so two groups on one page don't interfere.

Trade-offs

  • ↔

    useId adds a tiny cost and an extra hook per component; for a component that can only ever appear once (a page's main search box), a fixed id is fine and easier to read in tests.

  • ↔

    useDebugValue helps people debugging a shared hook but clutters small app hooks; add it to hooks other teams use.

Done when you can

  • I connect labels, hints and errors with ids from useId.

  • I know why useId matches between server and client and random ids don't.

  • I never use useId for list keys.

  • I can add a useDebugValue label with a lazy format function.

  • I can name every built-in hook and where it is taught.