Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Concurrent Features in React: useTransition and useDeferredValue

Concurrent Features: useTransition and useDeferredValue

~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

React 18 introduced a new concept: concurrent rendering – React can INTERRUPT a render already in progress to handle a more urgent update in between (e.g. a keystroke), and resume afterward. useTransition and useDeferredValue are the two hooks that let YOU tell React which updates are "urgent" and which "can wait".

The scenario: an expensive search

Our product search currently only filters 6 products on a page – blazing fast, no problem. But imagine the search (as in many real apps) filtered a LARGE, client-side-held list, say 5,000 entries with expensive match highlighting. Without concurrent features, EVERY keystroke would briefly "freeze" the entire UI until filtering finishes – the input field itself would feel laggy, even though ONLY the results list is doing the expensive work.

A simulated expensive list to experiment with

To experience both the problem AND the fix concretely, we'll build a new, standalone demo page SearchDemoPage with an artificially slowed-down filter function – independent of ProductListPage, purely for learning purposes:

src/pages/SearchDemoPage.jsx
import { useState } from 'react';

const ITEMS = Array.from({ length: 5000 }, (_, i) => `Item No. ${i + 1}`);

// Artificially slowed down to make the effect noticeable -
// in a real app, the slowness would be a side effect of real work
// (complex rendering, large DOM diffs), not a deliberate slowdown like this.
function filterItemsSlowly(query) {
  const filtered = ITEMS.filter((item) => item.toLowerCase().includes(query.toLowerCase()));
  const start = performance.now();
  while (performance.now() - start < 200) {
    // deliberate block, simulates 200ms of expensive rendering work
  }
  return filtered;
}

function SearchDemoPage() {
  const [query, setQuery] = useState('');
  const results = filterItemsSlowly(query);

  return (
    <div>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search 5,000 items..."
      />
      <p>{results.length} matches</p>
      <ul>
        {results.slice(0, 50).map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchDemoPage;

Add a new route /search-demo in App.jsx pointing to SearchDemoPage (analogous to the other <Route> entries). Type quickly into the search field in the browser – EVERY character feels noticeably SLUGGISH, the input field "lags behind" your typing speed.

The fix, part 1: useTransition

useTransition marks a state update as "NOT urgent" – React is allowed to interrupt it, delay it, or even skip intermediate updates on rapid successive changes, in order to handle more urgent work (rendering a keystroke) first:

src/pages/SearchDemoPage.jsx
import { useState, useTransition } from 'react';

const ITEMS = Array.from({ length: 5000 }, (_, i) => `Item No. ${i + 1}`);

function filterItemsSlowly(query) {
  const filtered = ITEMS.filter((item) => item.toLowerCase().includes(query.toLowerCase()));
  const start = performance.now();
  while (performance.now() - start < 200) {}
  return filtered;
}

function SearchDemoPage() {
  const [inputValue, setInputValue] = useState('');
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const results = filterItemsSlowly(query);

  function handleChange(event) {
    const value = event.target.value;
    setInputValue(value); // urgent: the input field must respond IMMEDIATELY
    startTransition(() => {
      setQuery(value); // not urgent: allowed to be delayed/interrupted
    });
  }

  return (
    <div>
      <input
        value={inputValue}
        onChange={handleChange}
        placeholder="Search 5,000 items..."
      />
      {isPending && <p>Searching...</p>}
      <p>{results.length} matches</p>
      <ul>
        {results.slice(0, 50).map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchDemoPage;

The key trick: TWO separate state values. inputValue updates IMMEDIATELY on every keystroke (the input field always feels responsive), query – the value that drives the expensive filtering – gets wrapped in startTransition(...) and is allowed to lag "in the background". isPending indicates whether a transition is currently running, for a loading hint.

The alternative: useDeferredValue

For EXACTLY this pattern (a value allowed to "catch up later"), there's an even more direct hook, useDeferredValue – it avoids the second state and the startTransition wrapping:

import { useState, useDeferredValue } from 'react';

function SearchDemoPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const results = filterItemsSlowly(deferredQuery);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search 5,000 items..."
      />
      {isStale && <p>Searching...</p>}
      <p>{results.length} matches</p>
      {/* ... rest as before */}
    </div>
  );
}

useDeferredValue(query) returns a "deferred twin" of query: on an urgent update, it keeps returning the OLD value at first, and only updates itself once React has "room to breathe" for it. isStale (query !== deferredQuery) replaces isPending from the useTransition approach here.

HookWhen to use it
useTransitionYou EXPLICITLY control which set...() update gets wrapped in startTransition – more flexible, more code.
useDeferredValueYou "defer" an ALREADY existing value (prop or state) – less code, but you don't need control over the original setState call.

Achtung: Concurrent features do NOT make the COMPUTATION itself faster – the 200ms of filter work still takes 200ms. They only change the PRIORITIZATION: urgent updates (typing) get handled first, non-urgent ones (the results list) are allowed to wait. For ACTUALLY faster computation, you'd need other tools (web workers, more efficient algorithms, server-side filtering, virtualization from the last chapter).

Tipp: Rule of thumb: useTransition/useDeferredValue are worth it for interactions that (a) genuinely require noticeable compute time AND (b) compete for the SAME render time as faster, independent interactions (typing, clicking). For most everyday UI updates, they're unnecessary complexity – as with the profiler: MEASURE first, then decide.