Interruptible Updates in React 18: useTransition vs useDeferredValue
React 18 lets you mark state updates as interruptible instead of debouncing them. This post explains when to use useTransition versus useDeferredValue, with a DevTools workflow to verify the split on a throttled CPU profile.
07 May 2026, 06:20 UTC

The problem: a slider that fights back
You wire a range slider to a chart that renders 10,000 points. Dragging the slider stutters on a mid-range phone: every input event triggers a full re-render of the chart, and the browser cannot paint the slider's new position until that render finishes. The classic fix is a debounce — wait 150 ms after the last input event before recomputing. On a fast desktop that delay is pure waste; on a slow phone it is barely enough. You end up guessing a number that should depend on the device, not on you.
React 18's concurrent features replace that guess with a priority label. Instead of delaying an update by a fixed time, you tell React the update is non-urgent and may be interrupted. The browser's main thread stays free for input and painting, and the heavy render happens in the gaps. This post covers the two APIs that do this — useTransition and useDeferredValue — and how to choose between them.
The mental model: interruptible rendering
Before React 18, rendering was effectively synchronous from the user's point of view: once a state update started, React finished the render — reconciliation, meaning diffing the old and new element trees — before the browser could paint. In React 18 and later, the scheduler can pause that work mid-render, handle a more urgent update such as a keystroke or click, and resume afterward. The key term is transition: a group of state updates marked as interruptible.
Both APIs are opt-in and backward compatible. Updates you do not mark behave exactly as before, so adopting them carries no migration cost for the rest of the app.
useTransition: when you own the setter
Use useTransition when you control the state update that triggers the expensive work. It returns a pair: isPending, true while the transition is in flight, and startTransition, a function you wrap around the non-urgent setState calls.
function PriceChart({ points }) {
const [range, setRange] = useState([0, 100]);
const [isPending, startTransition] = useTransition();
const onSliderChange = (next) => {
setRange(next); // urgent: the slider thumb tracks the pointer
startTransition(() => {
setVisiblePoints(filterPoints(points, next)); // non-urgent
});
};
// render chart from visiblePoints, dim it while isPending
}
The slider's own state updates at normal priority, so the thumb follows the pointer immediately. The derived data updates inside the transition, so React can abandon a stale render when the user drags again.
useDeferredValue: when the value arrives from above
Use useDeferredValue when you do not control the setter — the query comes from a parent, a URL search parameter, or a form library. It takes a fast-changing value and returns a deferred copy that lags behind while renders are heavy:
function Results({ query, items }) {
const deferredQuery = useDeferredValue(query);
const visible = useMemo(
() => items.filter((i) => i.name.includes(deferredQuery)),
[items, deferredQuery]
);
return <HeavyList items={visible} />;
}
The parent updates query urgently; this component keeps rendering with the previous deferredQuery until the new one is ready. Pair it with useMemo on the derivation: useDeferredValue only delays when the result is committed, not the computation itself.
Worked example: verify the split in DevTools
Create a minimal app. Run these in your project terminal; no special permissions are needed, and concurrency-demo is a placeholder for any directory name you like:
npm create vite@latest concurrency-demo -- --template react
cd concurrency-demo
npm install
npm run dev
Replace src/App.jsx with a component that renders a filter input plus a table of 5,000 generated rows, using the useDeferredValue pattern above. Then verify in the browser:
- Install React DevTools, open its Profiler/Components settings, and enable "Highlight updates when components render".
- In Chrome DevTools, open the Performance panel, set CPU throttling to 6× slowdown (gear icon), and record a profile while typing quickly into the input.
- Expected check: urgent renders (the input) and transition renders (the table) are highlighted in different colors, and the input's keystroke-to-paint time stays low even while the table re-renders. In the Performance trace, long script blocks should not line up with input events.
Risk: if the bottleneck is not React render work — for example forced synchronous layout from reading offsetHeight after DOM writes, or one huge DOM mutation — transitions will not help. Profile first to confirm the time is spent in component render, not layout.
Trade-offs and limits
- Render-bound only. Transitions split React's reconciliation work; they do not speed up layout, paint, or CSS selector matching.
- Do not over-wrap. Marking every update as a transition can starve updates that are actually urgent. Reserve it for updates you have measured as slow.
- useDeferredValue does not skip work. The expensive derivation still runs on the main thread; it just commits later. For truly heavy computation, move it to a Web Worker.
- Suspense interaction. Both APIs work with Suspense boundaries; a transition that suspends keeps showing previous content instead of a fallback, which is usually what you want for filtering and sorting.
How to decide
Ask one question: do I control the state setter? If yes, useTransition also gives you isPending, which you can use to dim the stale list. If no, useDeferredValue is the drop-in. Either way, measure before and after on a throttled CPU profile. Both APIs are opt-in, so if one does not help, removing the wrapper is a one-line edit — there is no other state to undo.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.