React Concurrent Features
Keep user interfaces responsive during heavy rendering operations using useTransition, startTransition, and useDeferredValue.
TL;DR
- Prioritize urgent user keystrokes above heavy background
transitions. - Track pending execution states automatically using the
useTransitionhook. - Postpone expensive secondary re-renders with the non-blocking
useDeferredValueAPI.
Transitions with useTransition
useTransition HookMark state mutation as non-urgent background task.
const [isPending, startTransition] = useTransition();
const onTabChange = (nextTab: string) => {
startTransition(() => {
setActiveTab(nextTab);
});
};Pending Visual IndicatorDim UI or display spinner while transition runs.
<CardView opacity={isPending ? 0.7 : 1}>
{isPending && <Text>Updating list...</Text>}
<TabContent tab={activeTab} />
</CardView>Async Transition HandlerExecute async operations inside startTransition.
startTransition(async () => {
await mutateBackend();
setStep(2);
});Deferred Values with useDeferredValue
useDeferredValue APILag heavy component render behind fast input.
export function SearchBox() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<CardView>
<InputBase value={query} onChange={setQuery} />
<HeavyList query={deferredQuery} />
</CardView>
);
}Stale Content DetectionDetect when deferred value is lagging behind input.
const isStale = query !== deferredQuery;
return <CardView opacity={isStale ? 0.5 : 1} />;Initial Value in React 19Supply initial deferred fallback in React 19.
const val = useDeferredValue(input, 'initial');Concurrent Rendering Priorities
Urgent vs TransitionTyping is urgent; rendering 1,000 cards is transition.
// Urgent: keep typing responsive
setQuery(text);
// Transition: heavy graph filtering
startTransition(() => setFiltered(items));Interrupted RenderingReact discards outdated render if new input arrives.
// If user types 'a' then 'b', rendering 'a' aborts
const deferred = useDeferredValue(query);Concurrent NavigationTransition router pushes to avoid white flash.
startTransition(() => {
router.push('/heavy-analytics');
});Concurrency Pitfalls
Controlled Input LagNever wrap input text setters in transitions.
// BAD: Input feels sticky and laggy
startTransition(() => setText(e.target.value));Premature ConcurrencyFix slow algorithms before reaching for concurrency.
// Better: Paginate or virtualize first
<VirtualList items={heavyList} />Impure Render TrapsConcurrent React aborts renders; keep functions pure.
// BAD: Mutating variables outside component body
globalCounter += 1;Tips
- Wrap expensive search list filtering in
startTransitionso text input typing remains buttery smooth without frame drops. - Use
useDeferredValuewhen filtering lists driven by parent props where you do not have direct access to the state updater.
Warnings
- Never wrap controlled input text value updates in
startTransition; typing input state must always update urgently. - Remember that
useDeferredValuedoes not prevent rendering work; it merely yields thread control to urgent user events.
In Practice
Build an instant-response search input that filters thousands of records without freezing keystrokes using useDeferredValue.
- Bind text input state directly for urgent real-time typing.
- Pass query into useDeferredValue to decouple heavy list rendering.
- Detect stale rendering state by comparing current and deferred values.
- Dim the list container gracefully while background recalculations run.
type FilterProps = { items: string[] };
export function LiveFilter({ items }: FilterProps) {
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query);
const isStale = query !== deferred;
const matches = useMemo(() => {
return items.filter((i) => i.includes(deferred));
}, [items, deferred]);
return (
<CardView>
<InputBase value={query} onChange={setQuery} />
<CardView opacity={isStale ? 0.6 : 1}>
<ListView items={matches} />
</CardView>
</CardView>
);
}FAQ
Concurrent React allows React to interrupt, pause, resume, or abandon rendering work to prioritize urgent user interactions like typing or clicking.
Use useTransition when you control the state update and want a pending flag. Use useDeferredValue when you receive a prop or value from above and want to defer rendering.
Yes, for many UI rendering scenarios. Unlike debounce which waits for idle delays, startTransition renders immediately if device CPU capacity is available.