Concurrent Features
HardReact 18 introduced concurrent rendering, allowing React to prepare multiple versions of the UI simultaneously and interrupt rendering to handle more urgent updates. useTransition and useDeferredValue let you mark updates as non-urgent, keeping the UI responsive during expensive operations.
Interactive Visualization
Key Points
- Concurrent rendering lets React pause, resume, and abandon renders
- useTransition marks state updates as non-urgent transitions
- useDeferredValue defers re-rendering with a stale value until urgent work completes
- Transitions keep the current UI visible while the next one prepares in the background
- isPending from useTransition indicates when a transition is in progress
- Automatic batching in React 18 groups all state updates, not just event handlers
Code Examples
useTransition for Expensive Updates
import { useState, useTransition } from 'react' function SearchWithTransition() { const [query, setQuery] = useState('') const [results, setResults] = useState<string[]>([]) const [isPending, startTransition] = useTransition() const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value // Urgent: update the input immediately setQuery(value) // Non-urgent: update the filtered list in the background startTransition(() => { const filtered = heavyFilter(value) setResults(filtered) }) } return ( <div> <input value={query} onChange={handleChange} /> {isPending && <span>Updating...</span>} <ul> {results.map((r, i) => <li key={i}>{r}</li>)} </ul> </div> ) } function heavyFilter(query: string): string[] { const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`) return items.filter((item) => item.toLowerCase().includes(query.toLowerCase()) ) }
The input stays responsive because the expensive filter is wrapped in startTransition, which React interrupts for urgent input updates
useDeferredValue for Derived Content
import { useState, useDeferredValue, memo } from 'react' function SearchPage() { const [query, setQuery] = useState('') const deferredQuery = useDeferredValue(query) const isStale = query !== deferredQuery return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <div style={{ opacity: isStale ? 0.6 : 1 }}> <SearchResults query={deferredQuery} /> </div> </div> ) } const SearchResults = memo(function SearchResults({ query }: { query: string }) { const results = heavySearch(query) return ( <ul> {results.map((r, i) => <li key={i}>{r}</li>)} </ul> ) }) function heavySearch(query: string): string[] { const items = Array.from({ length: 5000 }, (_, i) => `Result ${i}`) return items.filter((item) => item.toLowerCase().includes(query.toLowerCase()) ) }
useDeferredValue keeps showing old results while new ones compute in the background, with opacity dimming to indicate staleness
Common Mistakes
- Wrapping every state update in startTransition when only expensive ones benefit
- Forgetting to use memo with useDeferredValue which is needed to skip re-rendering with the stale value
- Using transitions for time-sensitive updates like animations that need immediate feedback
Interview Tips
- Explain the difference between useTransition (wraps the setter) and useDeferredValue (wraps the value)
- Know that concurrent rendering is opt-in via createRoot and transitions
- Discuss real use cases: search filtering, tab switching, heavy list rendering