useTransition: Keeping React Responsive During Expensive State Updates
useTransition marks a state update as non-urgent so React can interrupt it if something more important comes in. That is the key to UIs that stay responsive while heavy work is running — without debouncing, throttling, or moving logic off-thread.
React treats all state updates as equally urgent by default. When a state update triggers an expensive re-render — filtering a large list, recalculating a tree, updating a complex chart — React works through it before doing anything else. The UI freezes. User interactions queue up and feel unresponsive.
This is especially noticeable with search or filter inputs. Every keystroke triggers a re-render of every matching result. On large datasets, the input itself lags because React is busy re-rendering the list.
useTransition lets you mark a state update as non-urgent, so React can prioritize urgent work (keeping the input responsive) and process the expensive work when the thread is free.
How It Works
const [isPending, startTransition] = useTransition();useTransition returns two things: a boolean that is true while the non-urgent work is in progress, and a function to wrap the expensive update in.
A Practical Example: Filtering 10,000 Items
Without useTransition, filtering a large list on every keystroke blocks the input:
function handleChange(e) {
setQuery(e.target.value);
setFilteredItems(items.filter(item => item.includes(e.target.value)));
}Both updates are urgent. React runs them both synchronously. If the filter is slow, the input lags.
With useTransition:
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // urgent — input updates immediately
startTransition(() => {
setFilteredItems(items.filter(item => item.includes(e.target.value)));
});
}setQuery runs at full priority — the input value updates without any delay. The filtering inside startTransition is non-urgent. React processes it when the browser has idle capacity, without blocking the input from responding to the next keystroke.
The isPending Flag
While the transition is in progress, isPending is true. Use it to show a loading indicator:
{isPending && <p>Loading...</p>}
<ul>
{filteredItems.map(item => <li key={item}>{item}</li>)}
</ul>The user sees their input respond instantly and a loading state appear. The filtered results update when ready.
This is how you show a loading state for synchronous work — filtering, sorting, tab switching — the same way you would for an API call, without reaching for debounce or setTimeout.
What startTransition Does Not Do
startTransition is not a background thread. The work still runs on the main thread. What changes is the priority React assigns to it relative to other work.
If a higher-priority update comes in while a transition is in progress, React will pause the transition, process the urgent update, and then resume. This is React's concurrent rendering model at work — work can be interrupted and resumed, not just queued.
When to Use useTransition
useTransition is worth reaching for when:
- A state update drives an expensive synchronous render (large lists, complex UI trees)
- You want to show a loading state for that work without an artificial delay
- User interactions feel sluggish because React is processing a slow update
It is not a substitute for virtualization (for very large lists), pagination, or actual data fetching optimizations. It helps with the rendering cost of synchronous work — not the cost of the work itself.
Practice what you just read.
Keep reading