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
forattribute of<label>: it points the label at an input'sid. - 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
useIdid, 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.
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.
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.
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.
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
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
Inspect with DevTools
Open the useDebugValue example in a new tab with React DevTools installed, select
Cartin the Components panel, and find the label. Then check the console: the format function ran because DevTools asked for it. - 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
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
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_-hintServer 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
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_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
Console
Nothing logged yet.Expected console output
Cart rendered with 2 itemsThe console proves the table is complete for React 19.2: every `use...` export of `react` is listed, plus `useFormStatus` from `react-dom`.
Preview
Console
Nothing logged yet.Expected console output
hooks exported by react: 19
missing from the table: []
useFormStatus in react-dom: functionBreak 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.
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>; }).
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-caser). 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, sodocument.querySelector("#" + id)works withoutCSS.escape. Still prefer refs over querying by id. - ▸
useIdis stable for the life of the component, so it's also a handy stable token for things like anamefor 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, souseDebugValueis mostly for summarising.
Remember this
- 1
Why ids matter. HTML connects elements by
id:<label htmlFor="email">points at<input id="email">, andaria-describedby="email-hint"tells a screen reader which text describes the input (see the /html course on forms and accessibility). Hard-codingid="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
**
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
**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).useIdbuilds 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 withcreateRoot(el, { identifierPrefix: "shop-" })(and the same option on the server) so their ids can't collide. - 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
The full set. React 19.2 exports **19 hooks from
react** and one fromreact-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 realreactpackage. - 6
Two names you may meet in old code or search results:
useFormStatefromreact-domis the old name ofuseActionState(renamed in React 19; it still works, but the development build warnsReactDOM.useFormState has been renamed to React.useActionState.), and functions namedunstable_...are experimental and not for apps.useis the odd one out: it's a hook-like API that can be called insideifstatements and loops (Topic 14.4).
Explain it without notes
Why can't you hard-code ids in a reusable component, and how does useId solve it?
Why does useId exist instead of using a counter or Math.random()?
What does useDebugValue do, and when should you pass a format function?
Name the hooks React 19.2 provides for effects and say how they differ.
Practice
Write a reusable Checkbox component with a label and a hint text, connected with useId, and render it three times.
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
- ↔
useIdadds 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. - ↔
useDebugValuehelps 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
useIdmatches between server and client and random ids don't.I never use
useIdfor list keys.I can add a
useDebugValuelabel with a lazy format function.I can name every built-in hook and where it is taught.