DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

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

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 an isPending flag 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

Top comments (0)