Concurrent Features

Hard

React 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

Related Concepts