23 useDeferredValue – Werte verzögern, UI responsiv halten

useTransition markiert Updates als nicht-dringend. Der Entwickler entscheidet explizit: “Dieser State-Update kann warten.” useDeferredValue löst das gleiche Problem aus einer anderen Perspektive: Statt Updates zu markieren, verzögert er Werte. React erhält zwei Versionen desselben Werts – eine aktuelle und eine verzögerte. Die aktuelle Version reagiert sofort, die verzögerte nur, wenn Zeit dafür ist.

Das Ergebnis ist identisch zu useTransition – responsive UIs trotz teurer Operationen. Aber der Mechanismus und die Anwendungsfälle unterscheiden sich. useDeferredValue ist deklarativ (“Dieser Wert soll verzögert sein”), useTransition ist imperativ (“Führe diese Updates verzögert aus”). Beide haben ihren Platz.

23.1 Das Problem: Schnelle Eingabe, langsame Verarbeitung

Das Szenario ist bekannt: Ein Suchfeld filtert eine große Liste. Der Nutzer tippt “react”. Bei jedem Buchstaben – “r”, “e”, “a”, “c”, “t” – wird die gesamte Liste neu gefiltert. 10.000 Einträge durchsuchen, Strings vergleichen, Array filtern, Child-Komponenten rendern.

function SearchableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  
  // Bei jedem Tastendruck neu filtern
  const filteredItems = items.filter(item => 
    item.title.toLowerCase().includes(query.toLowerCase()) ||
    item.description.toLowerCase().includes(query.toLowerCase())
  );
  
  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)} 
      />
      
      <List items={filteredItems} />  {/* 10.000 Items rendern */}
    </div>
  );
}

Das Problem: Die Filterung blockiert. Der Nutzer tippt “r”, React filtert, rendert 10.000 Items. Dann “e”, wieder filtern, wieder rendern. Das Input-Feld reagiert verzögert. Der Nutzer spürt Lag.

Mit useTransition würden wir setQuery in eine Transition wrappen. Aber was, wenn query von außen kommt? Was, wenn die Komponente den State nicht kontrolliert?

function SearchResults({ query }: { query: string }) {
  // query kommt als Prop – wir kontrollieren ihn nicht
  
  const filteredItems = items.filter(item => 
    item.title.toLowerCase().includes(query.toLowerCase())
  );
  
  // Wie verzögern wir die Filterung?
  return <List items={filteredItems} />;
}

Hier können wir useTransition nicht verwenden – wir kontrollieren nicht, wann query sich ändert. Die Parent-Komponente tut es. Wir können nur reagieren.

23.2 Die Lösung: Verzögerte Werte

useDeferredValue nimmt einen Wert und gibt eine verzögerte Version zurück.

const deferredQuery = useDeferredValue(query);

query ist immer aktuell – es ändert sich sofort, wenn sich der Input ändert. deferredQuery ist verzögert – es ändert sich erst, wenn React Zeit hat, es zu aktualisieren.

Die Komponente, überarbeitet:

function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  
  // Filtern mit verzögertem Wert
  const filteredItems = items.filter(item => 
    item.title.toLowerCase().includes(deferredQuery.toLowerCase())
  );
  
  return (
    <div>
      {query !== deferredQuery && <div>Suche...</div>}
      <List items={filteredItems} />
    </div>
  );
}

Jetzt passiert folgendes: Der Nutzer tippt “r”. query wird sofort “r”. deferredQuery bleibt noch beim alten Wert. Die Filterung läuft noch nicht. React verarbeitet dringende Updates (Input-Rendering). Dann, wenn Zeit ist, wird deferredQuery zu “r” aktualisiert, und die Filterung läuft – ohne das Interface zu blockieren.

Das Input-Feld bleibt responsive. Die Liste aktualisiert sich im Hintergrund.

23.3 Die API: Einfach und deklarativ

useDeferredValue hat die einfachste API aller Hooks:

const deferredValue = useDeferredValue(value);

Ein Parameter rein, ein verzögerter Wert raus. Das war’s.

Der verzögerte Wert ist immer vom gleichen Typ wie der ursprüngliche Wert. Wenn value ein String ist, ist deferredValue ein String. Wenn value ein Objekt ist, ist deferredValue ein Objekt (möglicherweise eine ältere Version).

const query = 'react';
const deferredQuery = useDeferredValue(query);
// deferredQuery ist auch ein String, aber möglicherweise 'reac' oder ''

const filters = { category: 'books', minPrice: 10 };
const deferredFilters = useDeferredValue(filters);
// deferredFilters ist auch { category, minPrice }, aber möglicherweise alte Werte

Der Wert ist “stale” (veraltet), wenn er nicht mit dem Original übereinstimmt:

const isStale = value !== deferredValue;

if (isStale) {
  // Der verzögerte Wert ist noch nicht aktualisiert
  // Zeige Loading-Indicator
}

Diese Staleness-Detection ist nützlich für UI-Feedback.

23.4 Praktische Anwendungsfälle

23.4.1 1. Suchfeld mit großer Ergebnisliste

Der klassische Fall. Nutzer tippt, Ergebnisse filtern sich live.

function SearchPage() {
  const [query, setQuery] = useState('');
  
  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)}
        placeholder="Suche..."
      />
      
      <SearchResults query={query} />
    </div>
  );
}

function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  
  // Filterung läuft mit verzögertem Wert
  const results = useMemo(() => 
    allProducts.filter(p => 
      p.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [deferredQuery]
  );
  
  const isSearching = query !== deferredQuery;
  
  return (
    <div className={isSearching ? 'opacity-50' : ''}>
      {isSearching && <Spinner />}
      <ProductList products={results} />
    </div>
  );
}

Das Input-Feld ist immer responsive. Die Produktliste aktualisiert sich im Hintergrund. Der Opacity-Effekt zeigt, dass die Suche läuft.

23.4.2 2. Live-Datenvisualisierung

Charts und Graphen, die auf Slider-Eingaben reagieren.

function DataDashboard() {
  const [dateRange, setDateRange] = useState({ start: 0, end: 100 });
  const deferredRange = useDeferredValue(dateRange);
  
  // Teure Berechnung mit verzögertem Wert
  const chartData = useMemo(() => 
    calculateChartData(allData, deferredRange),
    [deferredRange]
  );
  
  const isUpdating = dateRange !== deferredRange;
  
  return (
    <div>
      <RangeSlider 
        value={dateRange} 
        onChange={setDateRange}  // Sofort responsive
      />
      
      {isUpdating && <div>Berechne...</div>}
      
      <ComplexChart data={chartData} />  {/* Aktualisiert verzögert */}
    </div>
  );
}

Der Slider bewegt sich flüssig. Das Chart aktualisiert sich im Hintergrund, ohne das Sliding zu blockieren.

23.4.3 3. Tabellen-Filterung

Große Tabellen mit mehreren Filteroptionen.

function DataTable({ data }: { data: Row[] }) {
  const [filters, setFilters] = useState({
    category: '',
    minPrice: 0,
    inStock: false
  });
  
  const deferredFilters = useDeferredValue(filters);
  
  const filteredData = useMemo(() => {
    return data.filter(row => {
      if (deferredFilters.category && row.category !== deferredFilters.category) {
        return false;
      }
      if (row.price < deferredFilters.minPrice) {
        return false;
      }
      if (deferredFilters.inStock && !row.inStock) {
        return false;
      }
      return true;
    });
  }, [data, deferredFilters]);
  
  const isFiltering = filters !== deferredFilters;
  
  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      
      {isFiltering && <ProgressBar />}
      
      <table className={isFiltering ? 'opacity-60' : ''}>
        <tbody>
          {filteredData.map(row => (
            <TableRow key={row.id} data={row} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

23.4.4 4. Responsive Text-Editor

Ein Editor, der Live-Preview zeigt.

function MarkdownEditor() {
  const [markdown, setMarkdown] = useState('# Hello World');
  const deferredMarkdown = useDeferredValue(markdown);
  
  // Teures Parsing und Rendering
  const html = useMemo(() => 
    parseMarkdown(deferredMarkdown),
    [deferredMarkdown]
  );
  
  return (
    <div className="editor-layout">
      <textarea 
        value={markdown} 
        onChange={e => setMarkdown(e.target.value)}
      />
      
      <div 
        className="preview"
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </div>
  );
}

Das Textarea bleibt flüssig. Die Preview aktualisiert sich verzögert, ohne das Tippen zu stören.

23.5 Der Unterschied zu useTransition

Beide Hooks lösen das gleiche Problem – blockierende Updates – aber aus unterschiedlichen Perspektiven.

Aspekt useTransition useDeferredValue
Ansatz Imperativ: “Führe diese Updates verzögert aus” Deklarativ: “Dieser Wert soll verzögert sein”
Kontrolle State-Updates wrappen Werte wrappen
Anwendung Wenn Sie den State kontrollieren Wenn Sie mit Props oder externen Werten arbeiten
API startTransition(() => setState(...)) const deferred = useDeferredValue(value)
Feedback isPending Boolean Staleness-Check: value !== deferred

useTransition ist besser, wenn: - Sie den State-Update kontrollieren - Sie mehrere State-Updates als Einheit verzögern wollen - Sie explizite Kontrolle über das Timing benötigen

useDeferredValue ist besser, wenn: - Der Wert von außen kommt (Props, Context) - Sie die Filterung/Berechnung verzögern wollen, nicht den Input - Sie eine deklarative API bevorzugen

Oft können beide verwendet werden. Die Wahl hängt vom Kontext ab.

// Variante 1: useTransition
function SearchWithTransition() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (value: string) => {
    setQuery(value);  // Sofort
    
    startTransition(() => {
      const filtered = items.filter(i => i.name.includes(value));
      setResults(filtered);  // Verzögert
    });
  };
  
  return (
    <>
      <input value={query} onChange={e => handleChange(e.target.value)} />
      {isPending && <Spinner />}
      <List items={results} />
    </>
  );
}

// Variante 2: useDeferredValue
function SearchWithDeferred() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  const results = useMemo(() => 
    items.filter(i => i.name.includes(deferredQuery)),
    [deferredQuery]
  );
  
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {query !== deferredQuery && <Spinner />}
      <List items={results} />
    </>
  );
}

Beide funktionieren. Variante 1 ist expliziter (man sieht die Transition). Variante 2 ist deklarativer (der Wert ist verzögert, Punkt).

23.6 Staleness Detection und UI-Feedback

Der verzögerte Wert kann “stale” sein – nicht aktuell. Diese Information ist wertvoll für visuelles Feedback.

function SmartSearch({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  
  const results = useMemo(() => 
    performSearch(deferredQuery),
    [deferredQuery]
  );
  
  return (
    <div>
      {/* Methode 1: Spinner */}
      {isStale && <Spinner />}
      
      {/* Methode 2: Opacity */}
      <div className={isStale ? 'opacity-50 transition-opacity' : ''}>
        <Results data={results} />
      </div>
      
      {/* Methode 3: Overlay */}
      <div className="relative">
        <Results data={results} />
        {isStale && (
          <div className="absolute inset-0 bg-white/50 flex items-center justify-center">
            <Spinner />
          </div>
        )}
      </div>
    </div>
  );
}

Die Staleness ist binär – entweder aktuell oder nicht. Sie sagt nichts über den Fortschritt (wie weit die Aktualisierung ist). Für detailliertes Feedback kombinieren Sie mit anderen Techniken.

23.7 Integration mit useMemo

useDeferredValue und useMemo sind ein mächtiges Duo. Der verzögerte Wert als Dependency verhindert, dass teure Berechnungen bei jedem Render laufen.

function ExpensiveComponent({ data }: { data: Data[] }) {
  const deferredData = useDeferredValue(data);
  
  // Berechnung läuft nur, wenn deferredData sich ändert
  const processed = useMemo(() => {
    console.log('Processing data...');
    return data.map(item => expensiveTransform(item));
  }, [deferredData]);
  
  return <Chart data={processed} />;
}

Ohne useMemo würde die Berechnung bei jedem Render laufen – auch wenn deferredData noch nicht aktualisiert ist. Mit useMemo läuft sie nur, wenn deferredData sich ändert.

Wichtig: Verwenden Sie deferredData in der Berechnung UND als Dependency.

// ❌ Falsch: data in Berechnung, deferredData als Dependency
const processed = useMemo(() => 
  data.map(expensiveTransform),  // Verwendet data
  [deferredData]  // Dependency ist deferredData
);

// ✓ Richtig: deferredData überall
const processed = useMemo(() => 
  deferredData.map(expensiveTransform),  // Verwendet deferredData
  [deferredData]  // Dependency ist deferredData
);

23.8 Häufige Fehler und Fallstricke

Fehler 1: Verzögerter Wert ohne teure Operation

// ❌ Bringt nichts
const deferredQuery = useDeferredValue(query);
return <input value={deferredQuery} />;  // Input wird verzögert reagieren!

useDeferredValue ist nur sinnvoll, wenn der verzögerte Wert teure Operationen auslöst. Wenn Sie ihn direkt im UI rendern, wird das UI verzögert – genau das Gegenteil von dem, was Sie wollen.

Fehler 2: Originaler Wert in teurer Berechnung

// ❌ Falsch: query statt deferredQuery
const deferredQuery = useDeferredValue(query);
const results = items.filter(i => i.name.includes(query));  // Verwendet query!

Wenn die teure Operation den originalen Wert verwendet, bringt useDeferredValue nichts. Die Operation läuft trotzdem bei jedem Update.

Fehler 3: Erwartung von Debouncing

// ❌ useDeferredValue ist kein Debouncing
const deferredQuery = useDeferredValue(query);
// deferredQuery aktualisiert sich nicht nach 300ms, sondern wenn React Zeit hat

useDeferredValue verzögert nicht um eine feste Zeit. Es priorisiert. Wenn der Browser idle ist, kann die Aktualisierung sofort erfolgen. Wenn er beschäftigt ist, kann sie länger dauern.

Für echtes Debouncing verwenden Sie einen Custom Hook:

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

Fehler 4: Objekt-Vergleich für Staleness

// ❌ Objekte werden immer als verschieden erkannt
const filters = { category: 'books' };
const deferredFilters = useDeferredValue(filters);
const isStale = filters !== deferredFilters;  // Immer true!

React vergleicht Objekte mit Object.is – referenziell. Selbst wenn deferredFilters aktualisiert wurde, ist es ein anderes Objekt. Für Objekte verwenden Sie Deep Equality oder spezifische Felder:

const isStale = filters.category !== deferredFilters.category;

23.9 Performance-Betrachtungen

useDeferredValue verbessert die wahrgenommene Performance, nicht die tatsächliche. Die Berechnung läuft immer noch, nur verzögert. Die UI bleibt responsive, aber die Arbeit bleibt gleich.

Szenario Ohne useDeferredValue Mit useDeferredValue
User tippt “r” Input blockiert 100ms Input reagiert sofort
Filter läuft Während Input blockiert Im Hintergrund, später
Gesamtzeit 100ms 100ms (oder mehr)
User-Erfahrung Träge, frustrierend Flüssig, responsive

Die eigentliche Optimierung ist algorithmischer Natur – effizientere Filterung, Virtualisierung, Caching. useDeferredValue ist die UI-Optimierung zusätzlich zu algorithmischen Optimierungen.

// 1. Optimieren Sie zuerst
const efficientFilter = useMemo(() => {
  // Effiziente Datenstruktur, optimierter Algorithmus
  return createSearchIndex(items);
}, [items]);

// 2. Dann verzögern Sie
const deferredQuery = useDeferredValue(query);
const results = efficientFilter.search(deferredQuery);

23.10 Ein vollständiges Beispiel

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

function ProductCatalog() {
  const [searchQuery, setSearchQuery] = useState('');
  const [categoryFilter, setCategoryFilter] = useState<string>('all');
  const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
  
  return (
    <div className="catalog">
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <FilterPanel 
        category={categoryFilter} 
        onCategoryChange={setCategoryFilter}
        priceRange={priceRange}
        onPriceRangeChange={setPriceRange}
      />
      
      <ProductResults 
        query={searchQuery}
        category={categoryFilter}
        priceRange={priceRange}
      />
    </div>
  );
}

function ProductResults({ query, category, priceRange }: {
  query: string;
  category: string;
  priceRange: { min: number; max: number };
}) {
  // Verzögerte Werte
  const deferredQuery = useDeferredValue(query);
  const deferredCategory = useDeferredValue(category);
  const deferredPriceRange = useDeferredValue(priceRange);
  
  // Teure Filterung
  const filteredProducts = useMemo(() => {
    return allProducts.filter(product => {
      // Text-Suche
      if (deferredQuery) {
        const lowerQuery = deferredQuery.toLowerCase();
        const matchesName = product.name.toLowerCase().includes(lowerQuery);
        const matchesTags = product.tags.some(tag => 
          tag.toLowerCase().includes(lowerQuery)
        );
        if (!matchesName && !matchesTags) return false;
      }
      
      // Kategorie-Filter
      if (deferredCategory !== 'all' && product.category !== deferredCategory) {
        return false;
      }
      
      // Preis-Filter
      if (product.price < deferredPriceRange.min || 
          product.price > deferredPriceRange.max) {
        return false;
      }
      
      return true;
    });
  }, [deferredQuery, deferredCategory, deferredPriceRange]);
  
  // Staleness-Detection
  const isFiltering = 
    query !== deferredQuery ||
    category !== deferredCategory ||
    priceRange !== deferredPriceRange;
  
  return (
    <div className="results">
      {isFiltering && (
        <div className="filtering-indicator">
          <Spinner /> Filtere {allProducts.length} Produkte...
        </div>
      )}
      
      <div className={isFiltering ? 'opacity-50 transition-opacity' : ''}>
        <p>{filteredProducts.length} Produkte gefunden</p>
        
        <div className="product-grid">
          {filteredProducts.map(product => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      </div>
    </div>
  );
}

Dieser Katalog bleibt responsive. Suchfeld, Filter-Controls und Preis-Slider reagieren sofort. Die Produktliste aktualisiert sich im Hintergrund, mit visuellem Feedback während der Filterung.

23.11 Best Practices

1. Verwenden Sie den verzögerten Wert in teuren Operationen

// ✓ Richtig
const deferredQuery = useDeferredValue(query);
const results = expensiveSearch(deferredQuery);

// ❌ Falsch
const deferredQuery = useDeferredValue(query);
const results = expensiveSearch(query);  // Verwendet Original!

2. Kombinieren Sie mit useMemo

const deferredFilters = useDeferredValue(filters);
const results = useMemo(() => 
  applyFilters(data, deferredFilters),
  [data, deferredFilters]
);

3. Visuelles Feedback bei Staleness

const isStale = value !== deferredValue;
return (
  <div className={isStale ? 'opacity-60' : ''}>
    {isStale && <LoadingIndicator />}
    {content}
  </div>
);

4. Nicht für alles verwenden

// ❌ Unnötig bei schnellen Operations
const deferredName = useDeferredValue(name);
return <h1>{deferredName}</h1>;

// ✓ Nur bei teuren Operations
const deferredQuery = useDeferredValue(query);
const results = expensiveFilter(deferredQuery);

useDeferredValue ist React’s deklarative Antwort auf das Priorisierungs-Problem. Wo useTransition sagt “führe diese Updates später aus”, sagt useDeferredValue “dieser Wert ist nicht dringend”. Beide erreichen das gleiche Ziel – responsive UIs trotz teurer Operationen – aber mit unterschiedlichen Metaphern. Die Wahl hängt vom Kontext ab: Kontrollieren Sie den State-Update? useTransition. Arbeiten Sie mit einem externen Wert? useDeferredValue. Oft sind beide Ansätze möglich, und die Entscheidung ist eine Frage der Präferenz und des Codes-Stils.