React useDeferredValue: Lazy Search ohne Debounce
AI generated
</>
{ }
React 18 · Concurrent Mode · Hooks · Performance
React useDeferredValue
Lazy Search ohne Debounce

Debounce mit setTimeout ist eine pragmatische Krücke, keine Lösung. useDeferredValue aus React 18 verschiebt teure Render-Arbeit ohne feste Zeitfenster – der Browser bleibt reaktionsfähig, während die Ergebnisliste im Hintergrund aktualisiert wird.

12 Min. Lesezeit useDeferredValue · startTransition · Concurrent Mode · React.memo React 18+ · TypeScript

1. Das Problem mit Debounce in React

Wer eine Echtzeit-Suche in React implementiert, greift reflexartig zu Debounce: Ein useEffect mit setTimeout verzögert den teuren Filter-Durchlauf um 300 Millisekunden, sobald der Nutzer aufhört zu tippen. Das funktioniert, hat aber fundamentale Schwächen. Ein festes Zeitfenster ist eine Heuristik – auf einem schnellen Desktop-Rechner sind 300 ms zu lang, auf einem günstigen Mobilgerät zu kurz. Das Ergebnis ist ein spürbares Flackern oder, schlimmer, eine träge UI, die auf Tastendrücke erst mit Verzögerung antwortet.

Das zweite Problem liegt in der React-Integration selbst. Ein Debounce-Timer lebt außerhalb des Render-Zyklus. Er wird in einem useEffect mit Cleanup-Funktion verwaltet, was zu subtilen Bugs führt: Wenn der Nutzer schnell tippt, können mehrere Timer gleichzeitig aktiv sein. Wird die Komponente unmounted, bevor der Timer feuert, und der Cleanup läuft nicht korrekt, werden State-Updates auf eine bereits unmounted Komponente angewendet – eine häufige Quelle von Memory-Leak-Warnungen. useDeferredValue löst genau dieses Problem durch native Browser-Concurrency statt durch willkürliche Timer.

Der dritte Aspekt betrifft die Nutzererfahrung unter Last. Wenn das JavaScript-Bundle groß ist und der Haupt-Thread beschäftigt ist, kann ein 300-ms-Debounce die Blockierung verlängern statt verkürzen – der Timer startet erst, wenn der Haupt-Thread wieder frei ist. Mit Concurrent Mode und useDeferredValue priorisiert React den Input-Handler und setzt teure Render-Arbeit zurück, sodass die UI unter Last reaktionsfähig bleibt.

2. Concurrent Mode: Wie React 18 Prioritäten vergibt

React 18 führt mit dem Concurrent Mode ein grundlegend neues Scheduling-Modell ein. Statt synchron von oben nach unten zu rendern, kann React 18 Render-Arbeit unterbrechen, zurückstellen und in kleineren Einheiten abarbeiten. Das Schlüsselkonzept dabei ist das Rendering als unterbrechbare Arbeit: Wenn ein dringenderer Update – etwa ein Tastendruck – eintrifft, während React gerade eine teure Liste rendert, kann React die laufende Arbeit pausieren, den dringenden Update verarbeiten und dann die Liste weitermalen.

Dieses Scheduling basiert auf Prioritätslanes. React unterscheidet zwischen synchronen Updates (Benutzereingaben wie Klicks und Tastendrücke) und Transitions (Updates, die das Ergebnis einer Eingabe darstellen, aber nicht sofort sichtbar sein müssen). useDeferredValue und startTransition sind die zwei APIs, mit denen Entwickler diesem Scheduler mitteilen können, welche Arbeit zurückgestellt werden darf. Der Unterschied: startTransition markiert eine State-Update-Funktion als niedrig priorisiert, während useDeferredValue einen bereits vorhandenen Wert als "darf veraltet sein" markiert.

Konkret bedeutet das für eine Suchkomponente: Der Input-State (query) wird immer synchron aktualisiert, damit der Nutzer sofort sieht, was er tippt. Der abgeleitete Wert deferredQuery, der für den teuren Filter-Durchlauf verwendet wird, hinkt nach – aber nicht um eine feste Zeit, sondern so lange, wie der Browser braucht, um dringende Arbeit abzuarbeiten. Auf schnellen Geräten ist die Verzögerung nahezu null, auf langsamen angemessen.

3. useDeferredValue: Syntax und Grundprinzip

useDeferredValue hat eine sehr einfache Schnittstelle: Der Hook nimmt einen Wert entgegen und gibt eine verzögerte Version dieses Werts zurück. In React 18 ist das zweite Argument noch nicht vorhanden; ab React 19 kann ein initialer Fallback-Wert übergeben werden. Das Kernprinzip: React rendert die Komponente zunächst mit dem alten Wert (dem "stale" Wert), um die UI sofort zu zeigen, und plant dann im Hintergrund ein Re-Render mit dem neuen Wert. Wenn während dieses Hintergrund-Renders ein weiterer Update eintrifft, wird der laufende Hintergrund-Render verworfen und neu gestartet.

Wichtig: useDeferredValue hilft nur, wenn die Komponente, die den Wert konsumiert, tatsächlich teuer zu rendern ist. Wenn das Rendering trivial ist, fügt der Hook nur unnötige Komplexität hinzu. Der Hook ist kein magischer Performance-Schalter – er ist ein Werkzeug, das teure Renders explizit als niedrig priorisiert markiert und React erlaubt, dringende Arbeit vorzuziehen. In Kombination mit React.memo wird verhindert, dass die Kind-Komponente bei jedem Keystroke neu gerendert wird, solange der deferred Wert noch gleich ist.


import { useState, useDeferredValue, memo, useMemo } from 'react';

// Expensive list component — only re-renders when deferredQuery changes
const SearchResults = memo(function SearchResults({
  query,
  items,
}: {
  query: string;
  items: string[];
}) {
  // Simulate expensive filtering operation
  const results = useMemo(() => {
    if (!query) return items;
    return items.filter((item) =>
      item.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, items]);

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

export function LazySearch({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  // deferredQuery lags behind query — React prioritises the input update
  const deferredQuery = useDeferredValue(query);

  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Suche …"
      />
      {/* Visual feedback while deferred value catches up */}
      <div style={ { opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' } }>
        <SearchResults query={deferredQuery} items={items} />
      </div>
    </div>
  );
}

4. Lazy-Search-Implementierung Schritt für Schritt

Eine vollständige Lazy-Search-Implementierung besteht aus drei Bausteinen: dem Input-State, dem deferred Wert und der memoiserten Ergebniskomponente. Der Input-State wird über useState verwaltet und ist der einzige State, der synchron aktualisiert wird – damit der Nutzer sofort visuelles Feedback erhält. Der deferred Wert wird von useDeferredValue erzeugt und hinkt nach. Die Ergebniskomponente ist mit React.memo umhüllt und bekommt ausschließlich den deferred Wert übergeben, nie den aktuellen Query-State.

In der Praxis verwaltet man häufig nicht nur einen Suchbegriff, sondern auch Filterparameter. Das Muster bleibt identisch: Alle Parameter, die einen teuren Filter-Durchlauf auslösen, werden in ein Objekt gebündelt, dieses Objekt wird an useDeferredValue übergeben, und die Ergebniskomponente bekommt nur den deferred Wert. React erkennt dabei, dass ein Objekt durch Referenz verglichen wird – eine neue Objektreferenz bei jedem Render löst immer ein Re-Render der Kind-Komponente aus. Deshalb muss das Filterobjekt mit useMemo stabilisiert werden, bevor es an useDeferredValue übergeben wird.

Ein oft übersehenes Detail: useDeferredValue ist nur effektiv, wenn die Kind-Komponente tatsächlich das Rendering überspringen kann, wenn der Wert sich nicht geändert hat. Das erfordert React.memo auf der Kind-Komponente. Ohne React.memo rendert React die Kind-Komponente bei jedem Render der Eltern-Komponente neu – und der deferred Wert hat keinen Effekt auf die Performance.

5. React.memo als notwendiger Partner

React.memo ist die Voraussetzung dafür, dass useDeferredValue seine Wirkung entfaltet. Der Hook selbst schiebt nur den Wert nach; ob das dazu führt, dass tatsächlich weniger gerendert wird, hängt davon ab, ob die Kind-Komponente das Rendering überspringt, solange ihre Props unverändert sind. Genau das leistet React.memo: Es vergleicht die alten und neuen Props flach und verhindert das Re-Render, wenn keine Prop sich geändert hat.

Der Standard-Vergleich von React.memo ist eine flache Gleichheitsprüfung mit Object.is. Das bedeutet, dass primitive Werte wie Strings und Numbers zuverlässig verglichen werden, Objekte und Arrays aber nur per Referenz. Wer ein komplexes Filter-Objekt als Prop übergibt, muss dafür sorgen, dass die Referenz stabil ist – durch useMemo in der Eltern-Komponente. Alternativ kann man React.memo eine benutzerdefinierte Vergleichsfunktion übergeben, die einen tiefen Vergleich durchführt – das ist aber nur selten nötig und kann selbst Overhead erzeugen.


import { useState, useDeferredValue, memo, useMemo } from 'react';

interface FilterParams {
  query: string;
  category: string;
  minPrice: number;
}

interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

// Wrapped with memo — only re-renders when filter reference changes
const ProductList = memo(
  function ProductList({
    filter,
    products,
  }: {
    filter: FilterParams;
    products: Product[];
  }) {
    const results = useMemo(() => {
      return products.filter(
        (p) =>
          p.name.toLowerCase().includes(filter.query.toLowerCase()) &&
          (filter.category === '' || p.category === filter.category) &&
          p.price >= filter.minPrice
      );
    }, [filter, products]);

    return (
      <ul>
        {results.map((p) => (
          <li key={p.id}>
            {p.name} — {p.category} — {p.price}€
          </li>
        ))}
      </ul>
    );
  }
);

export function ProductSearch({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('');
  const [minPrice, setMinPrice] = useState(0);

  // Stable object reference — only changes when params change
  const filter = useMemo(
    () => ({ query, category, minPrice }),
    [query, category, minPrice]
  );

  // Deferred filter — React may render with old filter first
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Produktname …" />
      <input value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Kategorie …" />
      <input type="number" value={minPrice} onChange={(e) => setMinPrice(Number(e.target.value))} />
      <div style={ { opacity: isStale ? 0.6 : 1 } }>
        <ProductList filter={deferredFilter} products={products} />
      </div>
    </div>
  );
}

6. Stale-State-Feedback: Ladezustand anzeigen

Wenn useDeferredValue eingesetzt wird, gibt es immer einen Moment, in dem der angezeigte Wert nicht dem aktuellen Input entspricht. Dieser Moment ist der "stale"-Zustand. React stellt keine eingebaute API bereit, um diesen Zustand zu erkennen – aber er lässt sich trivial ableiten: Wenn der aktuelle Query-State und der deferred Wert nicht identisch sind, ist der Inhalt veraltet. Eine einfache Opazitätsreduzierung mit einer CSS-Transition vermittelt dem Nutzer dieses Feedback ohne aufwändige Lade-Spinner.

Für ausgefeilteres Feedback kann man useTransition kombinieren, das einen expliziten isPending-Boolean zurückgibt. Der Unterschied: useTransition wird am Aufrufort des State-Updates eingesetzt und markiert das Update selbst als Transition. useDeferredValue dagegen wird auf dem Wert selbst eingesetzt und ist dann sinnvoll, wenn man auf einen Wert zugreifen muss, den man nicht selbst kontrolliert – etwa eine Prop, die von einer Eltern-Komponente kommt. Der Vergleich query !== deferredQuery ist in beiden Fällen ein zuverlässiger Weg, den Stale-Zustand zu erkennen.

In komplexeren Anwendungen empfiehlt es sich, den Stale-Zustand auch für Screen-Reader-Nutzer kenntlich zu machen. Ein aria-busy="true" auf dem Ergebniscontainer, das gesetzt wird, solange der stale-Zustand aktiv ist, informiert assistive Technologien darüber, dass der Inhalt gerade aktualisiert wird. Das ist eine einfache, aber wichtige Ergänzung für barrierefreie Suchen.

7. useDeferredValue vs. Debounce im direkten Vergleich

Die Wahl zwischen useDeferredValue und Debounce hängt vom Kontext ab. Debounce ist ein festes Zeitfenster – es funktioniert deterministisch und ist einfach zu verstehen. Es eignet sich gut für Netzwerk-Requests, weil man dort explizit vermeiden will, zu viele Requests abzufeuern. Für teure Render-Operationen im Client ist es eine schlechte Wahl, weil es weder mit der React-Render-Pipeline interagiert noch auf Geräteleistung adaptiert.

Kriterium Debounce (setTimeout) useDeferredValue
Zeitfenster Fest (z.B. 300 ms) Adaptiv – je nach Browser-Last
React-Integration Außerhalb des Renderzyklus Concurrent-Mode-nativ
Memory-Leaks Manueller Cleanup nötig Kein Cleanup, React verwaltet alles
Netzwerk-Requests drosseln Ideal Nicht geeignet (kein Request-Throttling)
Teure Client-Renders Suboptimal Ideal in Kombination mit React.memo

In der Praxis kombiniert man beide Ansätze: useDeferredValue für den Render-Teil (die teure Ergebnisliste), und Debounce für den Netzwerk-Request (die Autocomplete-API). Das ergibt die optimale Nutzererfahrung: sofortiges Input-Feedback, verzögertes aber reaktionsfähiges Client-Rendering, und kontrollierte API-Abfragen.

8. Typische Fallstricke und wie man sie vermeidet

Der häufigste Fehler beim Einsatz von useDeferredValue ist das Fehlen von React.memo auf der Kind-Komponente. Ohne Memoisierung rendert React die Kind-Komponente bei jedem Render der Eltern-Komponente neu – und der deferred Wert hat keinerlei Effekt. Der Hook verzögert nur den Wert, nicht das Rendering. Das Rendering muss durch React.memo (oder useMemo für teure Berechnungen) selbst verhindert werden.

Ein zweiter häufiger Fallstrick: Das Objekt, das an useDeferredValue übergeben wird, wird bei jedem Render neu erstellt. Da React Objekte per Referenz vergleicht, sieht der Hook bei jedem Render ein "neues" Objekt und plant ein Re-Render – auch wenn sich die Werte nicht geändert haben. Die Lösung ist useMemo, um das Objekt zu stabilisieren. Primitive Werte (Strings, Numbers, Booleans) haben dieses Problem nicht – sie werden per Wert verglichen.

Ein dritter Fallstrick: useDeferredValue hilft nicht bei Netzwerk-Requests. Der Hook verschiebt Render-Arbeit, beeinflusst aber nicht, wann Requests ausgelöst werden. Wenn ein useEffect den Query-State beobachtet und bei jeder Änderung einen Request feuert, wird useDeferredValue das nicht verhindern. Für Request-Throttling ist Debounce oder ein dediziertes Throttling-Pattern (z.B. via AbortController) die richtige Wahl.


import { useState, useDeferredValue, memo, useMemo, useEffect } from 'react';

// WRONG: object created on every render — useDeferredValue sees a new reference
// each time and always schedules a re-render
function BadExample({ query }: { query: string }) {
  // New object reference on every render!
  const filter = { query, active: true };
  const deferredFilter = useDeferredValue(filter); // Always "new"
  // ...
}

// RIGHT: stabilise object with useMemo before passing to useDeferredValue
function GoodExample({ query }: { query: string }) {
  const filter = useMemo(() => ({ query, active: true }), [query]);
  const deferredFilter = useDeferredValue(filter); // Stable reference
  // ...
}

// RIGHT: combine useDeferredValue for rendering + debounce for network
function SearchWithAPI({ items }: { items: string[] }) {
  const [inputValue, setInputValue] = useState('');
  const [apiQuery, setApiQuery] = useState(''); // debounced for API
  const deferredInput = useDeferredValue(inputValue); // deferred for rendering

  // Debounce only the API call — not the render
  useEffect(() => {
    const timer = setTimeout(() => setApiQuery(inputValue), 400);
    return () => clearTimeout(timer);
  }, [inputValue]);

  // Use deferredInput for expensive client-side filtering
  const clientResults = useMemo(
    () => items.filter((i) => i.includes(deferredInput)),
    [deferredInput, items]
  );

  return <div>{/* render clientResults */}</div>;
}

9. TypeScript-Integration und Typsicherheit

useDeferredValue ist vollständig in den React-Typdefinitionen erfasst. Der Hook ist generisch und leitet den Rückgabetyp automatisch aus dem übergebenen Wert ab – keine manuelle Typannotation nötig. In TypeScript-Projekten empfiehlt es sich, die Filterparameter als Interface oder Type-Alias zu definieren und diesen Typ konsequent für den State, den useMemo-Wert und die Kind-Komponenten-Props zu verwenden. Das stellt sicher, dass der Typ beim Hinzufügen neuer Filterparameter an allen Stellen konsistent aktualisiert wird.

Ein häufiger TypeScript-Fehler beim Einsatz von React.memo mit generischen Komponenten: Der Typ der gememoisierten Komponente verliert seine Generizität. Das lässt sich umgehen, indem man die Komponente erst typisiert und dann separat mit React.memo umhüllt, statt beides in einem Ausdruck zu tun. Alternativ kann man die Komponente als Arrow-Function mit explizitem Typ annotieren. Die TypeScript-Typdefinitionen für useDeferredValue ab React 19 schließen den optionalen zweiten Parameter (initialValue) ein – in React 18-Projekten führt die Übergabe eines zweiten Arguments zu einem Typfehler.

Ein guter Ansatz für typsichere Suchen ist die Verwendung eines Discriminated-Union-Types für den Zustand der Suche: { status: 'idle' } | { status: 'searching'; query: string } | { status: 'results'; results: T[] }. Dieser Ansatz macht den Stale-Zustand explizit typsicher und verhindert, dass man auf results zugreift, wenn der Status idle ist. In Kombination mit useDeferredValue entsteht so eine robuste, typsichere Sucharchitektur.

10. Zusammenfassung

useDeferredValue ist das richtige Werkzeug, wenn teure Client-seitige Render-Operationen die UI-Reaktionsfähigkeit beeinträchtigen. Der Hook verzögert einen Wert, sodass React zunächst dringende Updates (Tastatureingaben, Klicks) verarbeitet und dann im Hintergrund mit dem aktualisierten Wert neu rendert. Das Ergebnis ist eine UI, die unter Last reaktionsfähig bleibt – ohne feste Timer, ohne Memory-Leak-Risiko und ohne Interaktion mit dem React-Renderzyklus zu umgehen.

Die drei Voraussetzungen für eine effektive Anwendung: Erstens muss die Kind-Komponente mit React.memo umhüllt sein, damit das Rendering übersprungen werden kann, wenn der deferred Wert noch nicht aktualisiert wurde. Zweitens müssen Objekte, die an useDeferredValue übergeben werden, mit useMemo stabilisiert werden. Drittens ist useDeferredValue kein Ersatz für Debounce bei Netzwerk-Requests – beide Ansätze haben unterschiedliche Aufgaben und können kombiniert werden. Wer diese drei Punkte beachtet, kann Debounce-Heuristiken in React-Anwendungen vollständig durch natives Concurrent-Mode-Scheduling ersetzen.

useDeferredValue — Das Wichtigste auf einen Blick

Grundprinzip

Verzögert einen Wert, damit React dringende Updates (Input) zuerst verarbeitet und teure Renders im Hintergrund plant.

Pflichtpartner React.memo

Ohne React.memo auf der Kind-Komponente hat useDeferredValue keinen Effekt – das Rendering wird nicht übersprungen.

Objekte stabilisieren

Objekte immer mit useMemo stabilisieren bevor sie an useDeferredValue übergeben werden – sonst reagiert der Hook auf jede neue Referenz.

Kein API-Throttling

useDeferredValue drosselt keine Netzwerk-Requests. Für API-Calls Debounce verwenden, für teure Client-Renders useDeferredValue.

11. FAQ: React useDeferredValue

1Was macht useDeferredValue?
Gibt eine verzögerte Version eines Werts zurück. React rendert zunächst mit dem alten Wert und plant ein Hintergrund-Re-Render mit dem neuen Wert – dringende Updates werden priorisiert.
2Unterschied zu Debounce?
Debounce: festes Zeitfenster, außerhalb des Renderzyklus, manueller Cleanup. useDeferredValue: adaptiv, Concurrent-Mode-nativ, kein Cleanup. Für teure Renders useDeferredValue, für API-Calls Debounce.
3Warum React.memo Pflicht?
Ohne React.memo rendert die Kind-Komponente trotzdem bei jedem Eltern-Render neu. React.memo überspringt das Rendering, solange der deferred Wert gleich ist – das ist der eigentliche Performance-Gewinn.
4Stale-Zustand erkennen?
const isStale = query !== deferredQuery. Wenn true, wird noch mit dem alten Wert gerendert. Opazität reduzieren für visuelles Feedback, aria-busy setzen für Barrierefreiheit.
5Objekte als Argument?
Immer zuerst mit useMemo stabilisieren. Ohne useMemo wird bei jedem Render ein neues Objekt erstellt – useDeferredValue sieht immer einen neuen Wert und bringt keinen Vorteil.
6Für API-Calls geeignet?
Nein. useDeferredValue beeinflusst nur Render-Arbeit, nicht useEffect-Callbacks. Für API-Throttling Debounce oder AbortController nutzen.
7Ab welcher React-Version?
Seit React 18.0. Ab React 19 mit optionalem zweiten Parameter (initialValue) für SSR. In React 18 führt ein zweites Argument zu einem Typfehler.
8useDeferredValue vs. startTransition?
startTransition markiert eine Update-Funktion als niedrig priorisiert. useDeferredValue markiert einen vorhandenen Wert als verzögerbar – sinnvoll für Props, die man nicht selbst kontrolliert.
9TypeScript-Typisierung?
Vollständig typisiert, Typ wird automatisch abgeleitet. Filterparameter als Interface definieren und konsequent für State, useMemo und Props verwenden.
10Immer statt Debounce?
Nein. Beide lösen unterschiedliche Probleme. In vielen Apps kombiniert man: useDeferredValue für teure Client-Renders, Debounce für API-Requests.