React.js, part 7: useDeferredValue and useId — the quiet hooks that fixed real bugs
Part 7from the React.js series · 20 parts in all
Not every useful hook arrives with a keynote. Two of them quietly removed whole classes
of everyday problems: useDeferredValue gave back the responsive list without
restructuring state, and useId made SSR-safe element ids a non-issue.
useDeferredValue: the lazy version of a transition
A transition needs you to own the state update. Often you do not — a value arrives as a
prop, or from a component you cannot change. useDeferredValue lets you defer
any value:
function Results({ query }) {
// The list renders from the lagging value; typing stays instant because
// React keeps the previous deferred value while the new one catches up.
const deferred = useDeferredValue(query);
const stale = deferred !== query; // show a subtle "updating" state
return (
<div className={stale ? 'dim' : ''}>
<BigList filter={deferred} />
</div>
);
}
Two subtleties: the deferred value must be a primitive or a stable reference (deferring
a freshly built object defeats it), and the stale comparison is how you render
the "hmm, updating" affordance without a loading spinner.
useId: ids that survive hydration
Accessible markup pairs <label for> with a matching input id. On the
server you cannot generate a counter-based id safely — the client's counter would disagree
and hydration would mismatch. useId returns a stable id that matches on both
sides:
function EmailField() {
const id = useId();
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" aria-describedby={id + '-hint'} />
<small id={id + '-hint'}>We only use this for receipts.</small>
</>
);
}
The generated value is opaque (it contains colons) — do not parse it, and do not use it
as a CSS selector. Its only job is to be unique and hydration-stable, and as a bonus that
makes it perfect for aria-* wiring in list rows.
Next: Server Actions — forms that post to the server without hand-writing a fetch, JSON body, and error channel.