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-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 The list has ~1,200 deliberately-costly rows, so rendering it on every keystroke is real work. blocking (u
"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
Enter fullscreen mode Exit fullscreen mode
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
};
Enter fullscreen mode Exit fullscreen mode
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



