Introduction
The useTransition hook is your primary tool for keeping the UI responsive during expensive state updates. It returns a pending state and a function to wrap your updates.
Key Concepts
Signature:
tsxconst [isPending, startTransition] = useTransition();
isPending: Boolean indicating if a transition is in progressstartTransition: Function to wrap state updates you want to mark as transitions
Real World Context
In production apps, useTransition shines for:
- Tab switching with expensive content
- Filtering/sorting large datasets
- Real-time search with instant previews
- Page transitions in single-page apps
Deep Dive
How startTransition Works
When you call startTransition(callback), React:
- Executes the callback synchronously
- Marks all state updates inside as transitions
- Starts rendering the new state in the background
- Can interrupt this render if a more urgent update occurs
- Shows the old UI until the new one is ready
The isPending State
isPending becomes true immediately when you call startTransition and stays true until the transition completes. Use it to show loading indicators:
tsx<div style={{ opacity: isPending ? 0.5 : 1 }}> {/* Content that's being updated */} </div>
Transitions vs Urgent Updates
React distinguishes between:
| Update Type | Priority | Example | API |
|---|---|---|---|
| Urgent | High | Typing, clicking | Regular setState |
| Transition | Low | Search results, tab content | startTransition |
Important Constraints
- In React 19,
startTransitionsupports async callbacks, allowing you toawaitinside them and have the transition tracked - State updates outside
startTransitionin the same event handler are still urgent
tsxfunction handleClick() { // This is URGENT - happens immediately setInputValue(newValue); startTransition(() => { // This is a TRANSITION - can be interrupted setSearchResults(computeResults(newValue)); }); }
Common Pitfalls
- Wrapping the wrong updates: Only wrap the expensive, deferrable updates - not the urgent ones like input values.
- Not providing visual feedback: Users need to know something is happening. Always use
isPendingto show loading states. - Using with controlled inputs incorrectly: Never wrap the input's value update in a transition - only wrap derived state.
Best Practices
- Show visual feedback using
isPending(opacity, spinners, skeletons) - Keep input/interaction updates outside transitions
- Use for tab switching, filters, search results, and navigation
- Combine with React.memo to prevent unnecessary child re-renders
- Consider
useDeferredValuefor simpler cases where you don't needisPending
Summary
useTransition marks state updates as non-urgent, allowing React to keep the UI responsive. It returns isPending for loading states and startTransition to wrap your deferrable updates.
Code Examples
tsx
import { useState, useTransition, memo } from 'react';
// Memoized expensive component
const SlowList = memo(function SlowList({ text }: { text: string }) {
const items = [];
for (let i = 0; i < 500; i++) {
items.push(<SlowItem key={i} text={text} />);
}
return <ul className="slow-list">{items}</ul>;
});
function SlowItem({ text }: { text: string }) {
// Artificial slowdown
const startTime = performance.now();
while (performance.now() - startTime < 1) {}
return <li>{text}</li>;
}
export function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav className="tabs">
<TabButton
isActive={tab === 'about'}
onClick={() => selectTab('about')}
>
About
</TabButton>
<TabButton
isActive={tab === 'posts'}
onClick={() => selectTab('posts')}
>
Posts (slow)
</TabButton>
<TabButton
isActive={tab === 'contact'}
onClick={() => selectTab('contact')}
>
Contact
</TabButton>
</nav>
<div className="tab-content" style={{ opacity: isPending ? 0.6 : 1 }}>
{isPending && <div className="loading-overlay">Loading...</div>}
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <SlowList text="Posts" />}
{tab === 'contact' && <ContactTab />}
</div>
</div>
);
}