React 18 Concurrency: useTransition & useDeferredValue for Responsive Search UIs
Large lists can freeze while filtering. React 18’s useTransition and useDeferredValue let you keep typing responsive, deferring heavy renders. Read how, see a 10k‑item example, and learn the trade‑offs.
25 Jul 2026, 03:23 UTC

The UI Jank Problem in Large Lists
When a user types into a search box that filters a list of thousands of items, the browser may drop frames. Every keystroke triggers a state change, a re‑render of the list, and a DOM diff. On a low‑end device or when the filtering algorithm is heavy, the UI feels sluggish.
How useTransition Works
useTransition is a hook that marks a state change as low‑priority. React splits work into two lanes: SyncLane for urgent updates (clicks, typing) and TransitionLane for non‑urgent work (list filtering). The hook returns a tuple [isPending, startTransition] where isPending becomes true while the transition is queued, and false once the transition completes.
When the user types, the input change is a sync update. The expensive filter runs in the transition lane. React can abandon a transition if a newer sync update arrives, avoiding stale commits. This is different from debouncing, which simply delays work; React can cancel it mid‑flight.
What useDeferredValue Adds
Sometimes you cannot control the setter that provides the value you want to defer—e.g., a prop coming from a parent component. useDeferredValue creates a copy that lags behind the source value. The component renders immediately with the old value, then re‑renders with the new value after high‑priority work finishes. It accepts an optional timeoutMs (React 18.2+) to force a commit after a delay, which can prevent excessive jank if the scheduler never yields.
Putting It Together: 10k‑Item Search Example
Below is a minimal component that demonstrates a responsive search. The list contains 10,000 items. The query state is wrapped in startTransition, and the list receives a deferred copy of the query.
import { useState, useTransition, useDeferredValue } from 'react';
function SearchableList() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const deferredQuery = useDeferredValue(query, 200); // 200ms timeout
const filtered = React.useMemo(() => {
// Simulate heavy filtering
return items.filter(item => item.includes(deferredQuery));
}, [deferredQuery]);
return (
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value);
startTransition(() => {}); // triggers filtering as transition
}}
/>
{isPending && <span>Updating…</span>}
<ul>
{filtered.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
}
Key points:
- The
onChangehandler updatesquerysynchronously so the input feels instant. - Wrapping the filter logic in
useMemoensures it only runs whendeferredQuerychanges. - The spinner appears only while the transition is pending.
Trade‑offs and When to Avoid Transitions
Transitions add a small latency: the UI shows the old list while the new one is still computing. If the filtering completes in <100 ms, the perceived delay can feel slower than a synchronous update. Also, useTransition does not solve algorithmic complexity; an O(n²) filter will still block if it runs in the transition lane. Use transitions when the expensive work is I/O‑bound or when you can afford a brief stale UI.
Another consideration is auto‑batching. React 18 batches all state updates, even those inside promises or timeouts. useTransition opts out of this batching for the marked update, giving explicit priority control. If your code relies on synchronous flushes, you may need to adjust.
Practical Verification & Migration Notes
To confirm that transitions are working:
- Open React DevTools Profiler and enable "Record why each component rendered".
- Type in the search box and observe that the list component renders in the TransitionLane (yellow) while the input remains responsive.
- Use Chrome DevTools Performance panel with
performance.markaroundsetQueryto see the latency between keystroke and list update. - Throttle the CPU (e.g., 4× slowdown) and note that typing stays at ~60 fps while the list lags smoothly.
Migration checklist:
- Ensure
reactandreact-domare at ^18.0.0. - Wrap components that use these hooks with "use client" if you are in a Next.js App Router environment.
- Guard any code that imports
useTransitionoruseDeferredValuewith a version check or peerDependency. - Test on both desktop and mobile to gauge the perceived latency.
Conclusion
React 18’s useTransition and useDeferredValue give you fine‑grained control over UI responsiveness. For a searchable list of thousands of items, they let you keep typing snappy while the heavy filtering runs in the background. Remember the trade‑offs—stale UI, potential jank if the work is too heavy—and verify with the Profiler and performance marks before deploying to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.