Skip to content

React Tutorial Advanced React Concepts

useTransition and useDeferredValue: Keeping the UI Responsive

Lesson 123 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Some updates are urgent: typing in a box, clicking a button, pressing a key. The user expects an immediate reaction. Others can wait a moment: filtering a huge list, switching to a heavy tab. React lets you tell the two apart, so urgent work is never blocked by slow work.

The problem

If rendering a result takes 200 ms, and it happens on every key press, typing feels sticky, because React finishes the slow render before it can show the next character.

useTransition

useTransition marks a state update as a transition: non-urgent and interruptible. React renders the urgent updates first and keeps the screen responsive. If a new urgent update arrives, React can abandon the old transition render and start again.

App.jsx
JSX
const [isPending, startTransition] = useTransition();

startTransition(() => {
  setQuery(text);   // this update is non-urgent
});

isPending is true while the transition is rendering, so you can show a subtle hint.

App.jsx
JSX
// A deliberately slow row, to simulate an expensive list
function SlowRow({ text }) {
  const start = performance.now();
  while (performance.now() - start < 1) { /* 1 ms of fake work per row */ }
  return <li>{text}</li>;
}

const ALL = Array.from({ length: 300 }, (_, i) => "Item " + i);

function App() {
  const [text, setText] = React.useState("");     // urgent: the input
  const [query, setQuery] = React.useState("");   // non-urgent: the list filter
  const [isPending, startTransition] = React.useTransition();

  function handleChange(e) {
    setText(e.target.value);                      // updates immediately
    startTransition(() => setQuery(e.target.value)); // list updates when it can
  }

  const matches = ALL.filter((x) => x.toLowerCase().includes(query.toLowerCase()));

  return (
    <div>
      <input aria-label="Filter" placeholder="Type to filter" value={text} onChange={handleChange} />
      <p>{isPending ? "Updating list..." : "Matches: " + matches.length}</p>
      <ul style={{ opacity: isPending ? 0.5 : 1 }}>
        {matches.map((x) => <SlowRow key={x} text={x} />)}
      </ul>
    </div>
  );
}

The input state (text) updates at once, so typing feels fine. The list state (query) updates in a transition. While the slow list renders, the old list stays visible (dimmed) and isPending is true.

Rules for transitions:

  • Wrap state updates (the setter calls), not the whole handler logic.
  • You cannot use a transition to control a text input’s own value. Use a normal update for the input and a transition for the expensive state.
  • A transition only helps when there is expensive rendering to postpone. For network waiting it keeps old content visible while Suspense loads (Suspense).

useDeferredValue

When you do not control the code that sets the state (a prop from a parent, a library), use useDeferredValue. It gives you a lagging copy of a value: during urgent updates it keeps the old value, then catches up in the background.

App.jsx
JSX
function Results({ query }) {
  const items = Array.from({ length: 50 }, (_, i) => `${query} result ${i + 1}`);
  return (
    <ul>
      {items.map((x) => {
        const start = performance.now();
        while (performance.now() - start < 1) { /* slow */ }
        return <li key={x}>{x}</li>;
      })}
    </ul>
  );
}

function App() {
  const [query, setQuery] = React.useState("");
  const deferredQuery = React.useDeferredValue(query);   // lags behind during fast typing
  const stale = query !== deferredQuery;

  return (
    <div>
      <input aria-label="Search" placeholder="Type here" value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>Showing results for: {deferredQuery}</p>
      <div style={{ opacity: stale ? 0.5 : 1 }}>
        <Results query={deferredQuery} />
      </div>
    </div>
  );
}

query updates immediately (the input stays responsive); deferredQuery updates after the urgent work is done, and Results renders with it. If the user keeps typing, React skips outdated renders.

useTransitionuseDeferredValue
You controlThe code that sets stateA value you receive
Gives youisPending and startTransitionA deferred copy of the value
Typical useTab switches, filters, navigationSearch results driven by a prop

Do not use them as debouncing

Transitions do not wait a fixed time; they run as soon as there is capacity, so they do not reduce the number of network requests. For that, debounce the input (Search and Filtering).

When not to bother

If the list is small, nothing is slow and these hooks add complexity for no gain. Use them after you measure a real delay (Performance Optimization).

Key points

  • Transitions mark non-urgent updates so urgent ones (typing) stay smooth.
  • useTransition gives startTransition and isPending; useDeferredValue gives a lagging copy of a value.
  • Use them only for measured, expensive renders; they do not reduce network requests.