Recipe
React Transition Patterns
Keep your UI responsive during expensive state updates with startTransition and useTransition.
The Problem
Heavy filtering, sorting, or search operations on large lists block the main thread. Keystrokes lag, clicks feel unresponsive, and the UI freezes until React finishes rendering.
startTransition
Wrap the expensive state setter inside startTransition. React defers the render, keeping the input responsive. The pending state renders immediately; the heavy work follows without blocking.
import { startTransition, useState } from 'react'
function SearchList({ items }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
function handleChange(e) {
const next = e.target.value
setQuery(next) // immediate — input stays fast
startTransition(() => {
setFiltered(items.filter(i => i.includes(next)))
})
}
return (
<>
<input value={query} onChange={handleChange} />
<ul>{filtered.map(i => <li key={i}>{i}</li>)}</ul>
</>
)
}useTransition
When you need a loading indicator, useTransition exposes an isPending flag. Show a spinner or dim the stale content while the transition runs.
import { useTransition } from 'react'
function TabSwitcher() {
const [tab, setTab] = useState('home')
const [isPending, startTransition] = useTransition()
function switchTab(next) {
startTransition(() => setTab(next))
}
return (
<div className={isPending ? 'opacity-60' : ''}>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('settings')}>Settings</button>
<TabContent tab={tab} />
</div>
)
}When to Use
- Search-as-you-type with large datasets
- Tab or route transitions with heavy component trees
- Filtering or sorting tables with thousands of rows
- Any state update where the user expects instant feedback