useTransition and useDeferredValue

Intermediate topic. Most React apps do not need these hooks. If you have not read useMemo yet, start there. useTransition and useDeferredValue solve a specific performance problem: slow renders that block user input.

By default, every state update in React is treated as equally urgent. If an update triggers an expensive render (like filtering a list of 10,000 items), React works through that render before the browser can respond to further input. The UI can feel slow or stuck.

useTransition and useDeferredValue let you tell React which updates are less urgent so it can keep responding to user input while the expensive work catches up.

useTransition

useTransition returns two things:

  • isPending: a boolean that is true while the non-urgent update is still being processed.
  • startTransition: a function you call to wrap the non-urgent state update.
import { useState, useTransition } from 'react';

const [isPending, startTransition] = useTransition();

Typing in an input is urgent; the user must see their keystrokes immediately. Filtering a large list based on that input is less urgent. Split the two:

function Search() {
  const [query, setQuery] = useState('');
  const [filterText, setFilterText] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value); // urgent: update the input immediately

    startTransition(() => {
      setFilterText(e.target.value); // non-urgent: update the filtered list
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="Search..." />
      {isPending && <p>Updating results...</p>}
      <ResultsList filter={filterText} />
    </div>
  );
}

The input always shows the latest typed character. The ResultsList may lag slightly behind, but the user sees “Updating results…” while it catches up. This is far better than a frozen input.

A complete example

A filterable list of 5,000 items. Without a transition the input would stutter; with one it stays responsive:

import { useState, useTransition, memo } from 'react';

// Generate 5000 items once, outside the component
const ALL_ITEMS = Array.from({ length: 5000 }, (_, i) => ({
  id: i,
  text: `Item ${i + 1}`,
}));

const ItemList = memo(function ItemList({ filter }) {
  const filtered = ALL_ITEMS.filter(item =>
    item.text.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <ul>
      {filtered.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
});

function FilteredList() {
  const [query, setQuery] = useState('');
  const [filter, setFilter] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setQuery(e.target.value);

    startTransition(() => {
      setFilter(e.target.value);
    });
  }

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Type to filter 5000 items..."
      />
      {isPending ? (
        <p style={{ color: '#888' }}>Updating...</p>
      ) : (
        <p>{ALL_ITEMS.filter(i => i.text.toLowerCase().includes(filter.toLowerCase())).length} results</p>
      )}
      <ItemList filter={filter} />
    </div>
  );
}

export default FilteredList;

query drives the input. filter drives the list. They start from the same value, but filter updates inside startTransition, which lets React defer that work if higher-priority updates (like more keystrokes) arrive first.

useDeferredValue

useDeferredValue defers a value rather than a state update. It accepts a value and returns a deferred copy of it that lags behind during transitions.

Use it when you receive a value as a prop or from a context and cannot wrap the update in startTransition yourself:

import { useDeferredValue, memo } from 'react';

const SearchResults = memo(function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);

  const results = ALL_ITEMS.filter(item =>
    item.text.toLowerCase().includes(deferredQuery.toLowerCase())
  );

  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
});

deferredQuery will hold the previous value while React is still rendering the new one. Combined with memo, the component only re-renders when deferredQuery settles.

useTransition vs useDeferredValue

HookWhen to use
useTransitionYou own the state update. Wrap the setter call in startTransition.
useDeferredValueYou receive a value from props or context and cannot control when it updates.

Both achieve the same outcome: React prioritises urgent updates (input, clicks) and defers expensive renders. Which one you reach for depends on whether you control the update.

When you actually need these

Be honest with yourself before adding either hook. You need them when:

  1. A render is noticeably slow (tens of milliseconds or more).
  2. You have confirmed with the React DevTools Profiler that a specific state update is the bottleneck.
  3. useMemo alone does not solve the problem because the rendering work itself is expensive, not just the computation.

If you are filtering a list of 50 items, neither hook will make a visible difference. Do not add concurrency hooks as a default. Profile first, optimise second.

  • useMemo : cache expensive computed values to reduce render work
  • useCallback : stabilise function references passed to memoised children
  • useState : how state updates and re-renders work