Keep typing urgent
Paint the keystroke first; render heavy results in a transition.
React docsreact.dev
Reference: React useDeferredValue
React docsreact.dev
Reference: useTransition
From vercel-react-best-practices: rerender-use-deferred-value and 2 more.
Search 10,000 pages
Type quickly in each box. Bars show key-to-paint time.
× 10,000 = 200 ms
Filter every keystrokeBad
Last – · worst –
items.filter(i => fuzzyMatch(i, query))useDeferredValueGood
Last – · worst –
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const filtered = useMemo(
() => items.filter(i => fuzzyMatch(i, deferredQuery)),
[items, deferredQuery]
)
const isStale = query !== deferredQuery
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList results={filtered} />
</div>
</>
)
Scroll driving a heavy chart
Scroll each list, or press Auto-scroll.
setScrollY in handlerBad
Weeks in viewscrollY 0
Longest frame – · dropped 0
setScrollY(window.scrollY)startTransitionGood
Weeks in viewscrollY 0
Longest frame – · dropped 0
startTransition(() => setScrollY(…))import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
Loading flag vs isPending
Type fast. Both panes get identical requests and latencies.
isLoading flagBad
IdleSpinner flips 0
setIsLoading(true / false)useTransitionGood
IdleSpinner flips 0
const [isPending, startTransition] = useTransition()const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const latest = useRef('')
const handleSearch = (value) => {
setQuery(value)
latest.current = value
startTransition(async () => {
const data = await fetchResults(value)
if (value !== latest.current) return
startTransition(() => setResults(data))
})
}
{isPending && <Spinner />}
Why it matters, pros and cons
Why it matters
A sync render blocks the keystroke's frame. Transitions yield, so input paints first.
Pros
- Input stays at frame rate
isPendingreplaces a hand-rolled flag- No debounce delay on fast machines
Cons
- The work only moves; make the render cheaper first
- Without
useMemo, the urgent render still filters - Awaited responses can arrive out of order; guard them