Keep typing urgent

Paint the keystroke first; render heavy results in a transition.

React docsreact.dev
Reference: React useDeferredValue
API referenceRead docs ↗
React docsreact.dev
Reference: useTransition
API referenceRead docs ↗

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
              • isPending replaces 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