Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Advanced 11 min read

useTransition & useDeferredValue

Important things now, heavy things right after

In one sentence

With both of these you tell React: “this matters, that can wait.” Typing stays smooth while the heavy list catches up.

Picture typing into a search box – and on every letter React redraws a list of 5,000 entries. The field stutters, because React treats both jobs as equally important.

With these two hooks you say: “the letter has to appear immediately. The list may arrive a few milliseconds later.” React calls that a transition.

both.tsx
// Option A: useTransition – you control the UPDATE
const [isPending, startTransition] = useTransition();

function onType(e) {
  setQuery(e.target.value);              // urgent: the field
  startTransition(() => {
    setResults(search(e.target.value));  // may wait
  });
}

// Option B: useDeferredValue – you control the VALUE
const [query, setQuery] = useState("");
const laggingQuery = useDeferredValue(query);

// query        -> into the input (always current)
// laggingQuery -> into the heavy list (trails a little)
const results = useMemo(() => search(laggingQuery), [laggingQuery]);
Put another way

A waiter takes all the orders first (quick, visible) and only then walks to the kitchen (slow). Without a transition they would walk to the kitchen after every single order – and the next guest waits.

Which of the two?

choosing.txt
useTransition     ... when you hold the setState call yourself
                      (switching tabs, applying a filter, navigating)
                      Bonus: isPending -> you can show a loading hint

useDeferredValue  ... when the value arrives as a prop, or you just need
                      a copy that trails behind
                      No access to the setter required
tabs.tsx
function Tabs() {
  const [tab, setTab] = useState("start");
  const [isPending, startTransition] = useTransition();

  function switchTo(next: string) {
    startTransition(() => setTab(next));   // the click feels instant
  }

  return (
    <>
      <button onClick={() => switchTo("reports")}>Reports</button>
      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {tab === "reports" ? <HeavyReport /> : <Start />}
      </div>
    </>
  );
}
Without a transition the click freezes until the heavy tab has finished drawing.
Reach for it when …
  • +

    Filtering or sorting large lists while typing

  • +

    Switching tabs or views that draw a lot

  • +

    Showing a pending hint without throwing the old view away

Skip it when …
  • The input value itself – the typed text has to appear immediately

  • As a replacement for debouncing network requests

  • When nothing actually stutters: measure first, optimise second

Pitfall Never put the typed value in the transition

startTransition(() => setQuery(e.target.value)) breaks exactly what you set out to fix: the text in the field then appears with a delay. Urgent updates stay outside.

In more depth: what does “may wait” actually mean? optional

Normally React works through an update in one go – and the page does not respond meanwhile. A transition, on the other hand, may be interrupted: if something urgent arrives in between (a key press), React does that first and picks the transition back up afterwards.

So it is a priority, not a delay like setTimeout. Which is also why it does not help against too many network requests – for those you still need DebounceOnly act once things have been quiet for a moment – search after 300 ms without typing, for instance. or a query library.

Good to know React 19: transitions may await

startTransition(async () => { await save(); setStatus("done"); }) keeps isPending true across the whole await. The new form actions build on this.

Does it stick?

4 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

A smooth search over a big list

The list has 3,000 entries and is refiltered on every key press. Make the input respond immediately and let the list catch up – with useDeferredValue.

  • The text in the input appears immediately (query)
  • The list filters by the deferred value
  • While the list trails behind it is slightly dimmed
import { useDeferredValue, useMemo, useState } from "react";

const DATA = Array.from({ length: 3000 }, (_, i) => "Entry " + i);

export default function App() {
  const [query, setQuery] = useState("");
  // TODO: derive the deferred value

  const hits = useMemo(
    () => DATA.filter((d) => d.includes(query)).slice(0, 20),
    [query]
  );

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <input
        aria-label="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="e.g. 123"
      />
      <ul data-testid="list">
        {hits.map((h) => (
          <li key={h}>{h}</li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →