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.
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.
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.
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.
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.
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.
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>
);
}
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.
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).
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.
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
);
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;
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);
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.
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.