patternjavascriptreactModerate
useTransition marks state updates as non-urgent to keep the UI responsive
Viewed 0 times
React 18+
useTransitionstartTransitionconcurrent modeisPendingnon-urgent updateresponsive UI
Problem
Filtering a large list, navigating between heavy pages, or computing expensive derived state blocks the browser during re-render. The UI freezes while React works. The user experiences keystrokes that don't appear immediately or scroll that stutters.
Solution
Wrap non-urgent state updates in startTransition from useTransition:
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // urgent — update input immediately
startTransition(() => {
// non-urgent — React may interrupt this to handle urgent updates
setResults(expensiveFilter(allItems, value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList items={results} />
</>
);
}
// Also works with Suspense navigation
function App() {
const [page, setPage] = useState('home');
const [isPending, startTransition] = useTransition();
return (
<>
<nav>
<button onClick={() => startTransition(() => setPage('about'))}>
{isPending ? 'Loading...' : 'About'}
</button>
</nav>
<Suspense fallback={<Skeleton />}>
{page === 'home' ? <Home /> : <About />}
</Suspense>
</>
);
}
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // urgent — update input immediately
startTransition(() => {
// non-urgent — React may interrupt this to handle urgent updates
setResults(expensiveFilter(allItems, value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList items={results} />
</>
);
}
// Also works with Suspense navigation
function App() {
const [page, setPage] = useState('home');
const [isPending, startTransition] = useTransition();
return (
<>
<nav>
<button onClick={() => startTransition(() => setPage('about'))}>
{isPending ? 'Loading...' : 'About'}
</button>
</nav>
<Suspense fallback={<Skeleton />}>
{page === 'home' ? <Home /> : <About />}
</Suspense>
</>
);
}
Why
React's concurrent mode lets it interrupt low-priority renders when higher-priority updates (typing, clicking) arrive. startTransition marks an update as interruptible. React keeps the previous UI visible while computing the transition, making the app feel responsive even during heavy computation.
Gotchas
- Only state updates inside startTransition are marked as transitions — the update itself must be synchronous
- isPending becomes true immediately and stays true until the transition commits
- Transitions don't work for value updates that can't be interrupted — they only delay committing the new state
- In React 19, async functions in transitions are supported: startTransition(async () => { await save(); })
Code Snippets
Separate urgent from non-urgent updates
const [isPending, startTransition] = useTransition();
onChange={(e) => {
setInputValue(e.target.value); // urgent
startTransition(() => {
setFilteredList(filter(all, e.target.value)); // deferrable
});
}}Context
When a state update triggers expensive re-renders that cause the UI to feel unresponsive
Revisions (0)
No revisions yet.