Skip to content
·5 min read

useTransition for Non-Blocking UIs (And When useDeferredValue Fits)

Typing lags, tabs freeze, filters choke on big lists. React's transitions let urgent updates jump the queue while expensive ones render in the background.

ReactPerformanceHooksWeb Dev

The classic janky search page, in three lines:

const [query, setQuery] = useState("");
 
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<FilteredList query={query} /> // renders 10,000 rows per keystroke
// every keypress waits for the list -> typing feels broken

The input value and the list render are fighting for the same render slot, and React runs them first-come. Transitions let you tell React which update is urgent and which can be interrupted.

Step 1: Split urgent from expensive

Every janky screen has two updates. The urgent one the user needs now: the characters in the input, the active tab. The expensive one that can wait: the filtered list, the re-rendered results. Name both before writing code.

const [query, setQuery] = useState("");      // urgent: the input text
const [filter, setFilter] = useState("");    // expensive: drives the list

If you cannot name the two updates, a transition will not help, because you will end up marking the urgent one expensive.

Step 2: Mark the expensive update as a transition

useTransition returns isPending and startTransition. The state lives where it always did; only the update is flagged non-urgent:

import { useTransition } from "react";
 
function Search() {
  const [query, setQuery] = useState("");
  const [filter, setFilter] = useState("");
  const [isPending, startTransition] = useTransition();
 
  return (
    <>
      <input
        value={query}
        onChange={(e) => {
          setQuery(e.target.value);                  // urgent: types instantly
          startTransition(() => {
            setFilter(e.target.value);               // interruptible render
          });
        }}
      />
      <FilteredList query={filter} />
    </>
  );
}

The input updates on the normal priority, the list re-renders at low priority. React interrupts the list render every time a new keystroke arrives, so typing never queues behind a 200ms render of a list that is already obsolete.

Step 3: Show the pending state honestly

isPending is true while the transition render runs. Use it:

<div style={{ opacity: isPending ? 0.6 : 1 }}>
  <FilteredList query={filter} />
</div>

A dimmed list beats a frozen input, and it beats pretending the update was instant. Never fake it with setTimeout; the timeout still blocks the next keystroke and does not tell React anything about priority. That is the exact bug transitions replace.

Step 4: Reach for useDeferredValue at the leaf

When the expensive work happens inside a component you cannot edit, useDeferredValue is the same mechanism at a different insertion point:

function SearchBox({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
 
  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <FilteredList query={deferredQuery} />
    </div>
  );
}

The parent keeps one state; the component renders twice, once with the fresh value for the input and once with the lagging value for the list. Hooks go where the state lives, deferred values go where the rendering happens.

Step 5: Do not transition everything

Transitions are for updates that render a lot and stay renderable:

// good transition candidates
startTransition(() => setFilter(text));   // big list re-render
startTransition(() => setTab(next));      // heavy tab content
 
// keep these urgent
setName(e.target.value);                  // controlled input
setError(err);                            // user must see it now
formAction(data);                         // form submission

Form submissions, error states, and anything the user must see immediately stay urgent. Wrapping everything in startTransition makes nothing feel faster, it just hides which updates matter.

The whole pattern is one decision: which update is urgent. Split it, wrap the other one, wire up isPending. Typing goes back to feeling instant and the list catches up when it can.

React app with a janky search, tabs, or a list nobody wants to scroll? This fix is usually a day. Tell me about the screen and I will tell you which update to wrap first.