useTransition vs useDeferredValue: I Built a Lab Where You Can Feel Concurrent React
"Concurrent rendering keeps your UI responsive" is one of those React lines that means nothing until you feel a laggy input turn smooth. So I built a lab that filters a genuinely heavy list three ways β and a live main-t
"Concurrent rendering keeps your UI responsive" is one of those React lines that means nothing until you feel a laggy input turn smooth. So I built a lab that filters a genuinely heavy list three ways β and a live main-thread heartbeat that freezes to prove when the thread is blocked.
βΆ Live demo: https://concurrent-lab.vercel.app/
Source (React 19): https://github.com/dev48v/concurrent-lab
Same filter, three schedulings
The list has ~1,200 deliberately-costly rows, so rendering it on every keystroke is real work.
blocking (useState) β the filter runs synchronously; rendering the heavy rows blocks the main thread, so the input stutters and the heartbeat dot stops moving (the demo shows the worst frame gap β I've seen 270ms+).
useDeferredValue β the input binds to the live value; the list reads a deferred copy:
const [text, setText] = useState("");
const deferred = useDeferredValue(text); // lags behind, low priority
const list = useMemo(() => filter(deferred), [deferred]);
// input value={text} stays instant; while the list catches up, show it dimmed
useTransition β wrap the state update that triggers the heavy work:
const [isPending, startTransition] = useTransition();
const onChange = (v) => {
setText(v); // urgent: input updates now
startTransition(() => setQuery(v)); // non-urgent: list re-renders at low priority
};
With either, the input stays perfectly responsive and the heartbeat keeps ticking, while the list updates a beat later (with an isPending/"updating" badge and a dimmed old list).
When to use which
-
useDeferredValueβ you already have the value (a prop, a piece of state) and want a lower-priority copy for the expensive part. No control over the setter needed. -
useTransitionβ you own the state update that causes the heavy work, and you want anisPendingflag to show a spinner. Also great for tab/route switches.
Both are React saying: this update is low-priority β keep the UI interactive and finish it when you can. And unlike debouncing, React can interrupt the in-progress render if the user types again, instead of just delaying it.
If this made concurrent React click, a star helps others find it: https://github.com/dev48v/concurrent-lab
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.