useId & the Other Hooks
Five more built-in hooks round out the module. Each solves one narrow, specific problem — you’ll reach for most of them rarely, but when you need one, nothing else does the job.
flowchart TD
q1{"Need a unique id for
aria-* / label/input pairing?"} -->|yes| useId
q1 -->|no| q2{"Reading from a store
outside React (Redux, browser API)?"}
q2 -->|yes| useSyncExternalStore
q2 -->|no| q3{"Want to keep showing OLD UI
while new input is expensive to render?"}
q3 -->|yes| useDeferredValue
q3 -->|no| q4{"Writing a CSS-in-JS library
that injects style tags?"}
q4 -->|yes| useInsertionEffect
q4 -->|no| useDebugValue["useDebugValue
(labeling your own custom hook
in React DevTools)"] useId: SSR-safe unique ids
Section titled “useId: SSR-safe unique ids”useId generates a unique, stable id string for one component instance — built specifically so it matches between server-rendered HTML and the client’s hydration pass, which a hand-rolled counter or Math.random() cannot guarantee.
import { useId } from 'react';
function PasswordField() { const passwordHintId = useId(); return ( <> <label> Password: <input type="password" aria-describedby={passwordHintId} /> </label> <p id={passwordHintId}>The password should contain at least 18 characters</p> </> );}Hardcoding an id string works for exactly one instance of a component. Render PasswordField twice and a hardcoded id collides — invalid HTML (duplicate ids) and a broken aria-describedby association, because the browser’s accessibility tree can only associate an id with one element.
import { useId } from 'react';import { render, cleanup } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';
afterEach(cleanup);
// 🔴 Hardcoded id: every instance collides — invalid HTML (duplicate ids)// and broken `aria-describedby` association for assistive tech.function BadField() { return ( <> <input aria-describedby="hint" /> <p id="hint">Required</p> </> );}
// ✅ useId: unique per component instance, stable across the component's life.function GoodField() { const hintId = useId(); return ( <> <input aria-describedby={hintId} /> <p id={hintId}>Required</p> </> );}
test('hardcoded ids collide when a component renders more than once', () => { const { container } = render( <> <BadField /> <BadField /> </>, ); const ids = [...container.querySelectorAll('p')].map((p) => p.id); expect(ids).toEqual(['hint', 'hint']); // duplicate — invalid HTML});
test('useId gives each instance a distinct id', () => { const { container } = render( <> <GoodField /> <GoodField /> </>, ); const ids = [...container.querySelectorAll('p')].map((p) => p.id); expect(new Set(ids).size).toBe(2); // no collision});✓ hardcoded ids collide when a component renders more than once✓ useId gives each instance a distinct id
Test Files 1 passed (1) Tests 2 passed (2)Rendering more than one independent React app on the same page? Pass an identifierPrefix option to createRoot/hydrateRoot (and match it on the server’s renderToPipeableStream) so ids from the two apps can never clash:
const root1 = createRoot(document.getElementById('root1'), { identifierPrefix: 'app1-' });const root2 = createRoot(document.getElementById('root2'), { identifierPrefix: 'app2-' });Don’t use useId to generate keys for a list — it’s for accessibility attribute ids on a fixed number of elements per component instance, not per-item identity in a dynamic list (use each item’s own stable data id for that).
useSyncExternalStore: subscribing to state outside React
Section titled “useSyncExternalStore: subscribing to state outside React”When state lives in a store that isn’t React’s own (a global variable, a browser API, a third-party library like Redux or Zustand’s underlying subscription model), useSyncExternalStore is the hook that subscribes a component to it correctly — including a getServerSnapshot for SSR and protection against “tearing” (different components seeing different values from the same store during one render). This module’s sibling lesson, External Stores, covers the full API, the tearing problem, and the classic selector pitfall in depth — this is the one-paragraph pointer, not a repeat of it.
useDeferredValue: an initialValue for the first render (React 19)
Section titled “useDeferredValue: an initialValue for the first render (React 19)”useDeferredValue lets an expensive part of the UI lag behind an urgent update instead of blocking it. Since React 19, it takes a second argument, initialValue, used only for the component’s very first render:
import { useState, useDeferredValue } from 'react';
function SearchPage() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query, ''); // '' only on first render // ...}Without initialValue, react.dev is explicit that the hook “will not defer during the initial render” — the very first render just gets value directly. With it, the first render gets initialValue instead, letting you show an immediate placeholder before any real query has been typed.
// src/__tests__/useid-and-other-hooks.test.tsx (excerpt)test('useDeferredValue(value, initialValue) uses initialValue on the FIRST render, not a deferred empty value', () => { const seenOnFirstRender: string[] = []; function Search({ query }: { query: string }) { const deferred = useDeferredValue(query, 'placeholder'); seenOnFirstRender.push(deferred); return <output>{deferred}</output>; } render(<Search query="hello" />); // react.dev: "During the initial render, the returned deferred value will // be initialValue... If omitted, useDeferredValue will not defer during // the initial render." The first render shows the placeholder, not 'hello'. expect(seenOnFirstRender[0]).toBe('placeholder');});✓ useDeferredValue(value, initialValue) uses initialValue on the FIRST render, not a deferred empty value
Test Files 1 passed (1) Tests 1 passed (1)useDebugValue: labeling a custom hook in DevTools
Section titled “useDebugValue: labeling a custom hook in DevTools”useDebugValue adds zero runtime behavior — it exists purely so React DevTools can show a readable label next to a custom hook’s state instead of an opaque list of raw values:
function useOnlineStatus() { const [isOnline, setIsOnline] = useState(true); // ... useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline;}For a value that’s expensive to format into a readable string, pass a second format function — React only calls it when DevTools is actually inspecting that hook, not on every render:
useDebugValue(date, (d) => d.toDateString());You’ll only ever call this from inside a custom hook you’re authoring — it has nothing to add inside an ordinary component.
useInsertionEffect: CSS-in-JS authors only
Section titled “useInsertionEffect: CSS-in-JS authors only”useInsertionEffect fires before any layout effects, specifically so a CSS-in-JS library can inject <style> tags into the DOM before your component’s useLayoutEffect calls try to read layout that depends on those styles being present. Proven below against useLayoutEffect:
// src/__tests__/useid-and-other-hooks.test.tsx (excerpt)test('useInsertionEffect fires before useLayoutEffect', () => { const log: string[] = []; function Widget() { useInsertionEffect(() => { log.push('insertion'); }); useLayoutEffect(() => { log.push('layout'); }); return null; } render(<Widget />); expect(log).toEqual(['insertion', 'layout']);});✓ useInsertionEffect fires before useLayoutEffect
Test Files 1 passed (1) Tests 1 passed (1)react.dev is direct about the audience: this hook is “primarily intended for CSS-in-JS library authors.” It also comes with sharp caveats — you cannot update state from inside it, refs aren’t attached yet when it runs, and it only ever runs on the client. Application code should reach for useEffect or useLayoutEffect; if you’re not building a styling library, you almost certainly don’t need useInsertionEffect.
Mental Model
Section titled “Mental Model”These five hooks aren’t a random grab-bag — each one exists because a specific boundary needs crossing that the core hooks (useState, useEffect, useRef) weren’t designed for: useId crosses the server/client boundary for identifier generation, useSyncExternalStore crosses the React/non-React state boundary, useDeferredValue crosses the urgent/non-urgent rendering priority boundary, useInsertionEffect crosses the styles-before-layout timing boundary, and useDebugValue crosses the runtime/DevTools-inspection boundary. If you’re not standing at one of those five specific boundaries, you don’t need the hook.
Trade-offs
Section titled “Trade-offs”| Hook | Benefit | Cost |
|---|---|---|
useId | Guarantees no id collisions across instances or server/client hydration | One more hook call for something a hardcoded string “looks like” it could do |
useSyncExternalStore | Correct, tear-free subscriptions to non-React state, with SSR support | More setup than reading a global variable directly (but that directness is exactly what causes tearing) |
useDeferredValue(value, initialValue) | An immediate, sensible first paint instead of blocking on the real value | One more argument to remember; only matters for the very first render |
useInsertionEffect | Styles land before layout effects read them | Only useful inside a styling library; wrong tool for ordinary app effects |
Common Misconceptions
Section titled “Common Misconceptions”- “A hardcoded id string is fine as long as I only render the component once.” True until someone reuses the component a second time on the same page — proven above, that’s an immediate, invalid-HTML collision.
- “
useIdis for React keys in a list.” It isn’t — it’s for accessibility attribute ids on a fixed set of elements per instance. List keys need per-item data identity, notuseId. - “
useDeferredValuewithoutinitialValuealso defers on the first render.” react.dev states the opposite: with noinitialValue, the hook does not defer on that first render at all — it only starts deferring on subsequent updates. - “
useInsertionEffectis just an earlieruseEffectfor general use.” It’s a narrow tool for style injection with real restrictions (no state updates, refs not yet attached) — reaching for it in ordinary application code is almost always the wrong call.
Reading AI-generated code
Section titled “Reading AI-generated code”When generated code uses one of these smaller hooks (or should, but doesn’t), check for these before trusting it:
- Is an accessibility id (
aria-describedby,htmlFor/idpairing) hardcoded as a string literal instead of generated withuseId? - Is
useIdbeing used to key a.map()-rendered list, instead of each item’s own stable identifier? - Does a component read from an external store (a module-level variable,
window, a non-React library) directly during render instead of throughuseSyncExternalStore, risking tearing or a server/client mismatch? - Is
useDeferredValueused without considering whether the first render should showinitialValueor the real value immediately? - Is
useInsertionEffectused for something other than style injection — a sign the author reached for it without understanding its narrow purpose and restrictions?
🐛 Spot the bug
function PasswordField() {return ( <> <label> Password: <input type="password" aria-describedby="password-hint" /> </label> <p id="password-hint">At least 18 characters</p> </>);}Show the answer
The id "password-hint" is a string literal, not generated per instance. Render <PasswordField /> more than once on the same page — a login form and a signup form both open in a modal stack, say — and every instance shares the exact same id. That is invalid HTML (duplicate ids are not allowed) and breaks the accessibility association for every instance after the first: aria-describedby="password-hint" can only ever point assistive technology at the first <p id="password-hint"> in the document, not each field’s own hint. Proven in the probe: two instances of a hardcoded-id component render two <p> elements with the identical id "hint", while two instances of the same component using useId produce two distinct ids. The fix is exactly what this lesson opened with — replace the literal string with const passwordHintId = useId(); and use it for both the aria-describedby and the id.