Eine flüssige und reaktionsschnelle Benutzeroberfläche ist heute wichtiger denn je. Je größer und komplexer eine App wird, desto leichter gerät die UI ins Stocken – insbesondere bei vielen Interaktionen oder großen Datenmengen
React Hooks wie useTransition, useDeferredValue und useOptimistic helfen dabei, die Priorität von UI-Updates zu steuern und die Nutzererfahrung spürbar zu verbessern.
Ein typisches Beispiel ist ein Suchfeld, das bei jeder Eingabe sofort eine große Liste filtert. Jeder Tastendruck löst eine teure Berechnung und ein erneutes Rendern der Ergebnisliste aus – für Nutzer äußert sich das schnell als Verzögerung oder Ruckeln.
import { memo, useDeferredValue, useState } from 'react' import type { SearchResultListProps, GeoLocation } from '../types' const locations: GeoLocation[] = [ { name: 'Berlin' }, { name: 'Paris' }, // ... hundreds more ] const SearchResultList = memo(function SearchResults({ searchValue, }: SearchResultListProps) { const query = searchValue.toLowerCase() const filtered = locations.filter(location => { return location.name.toLowerCase().includes(query) }) return ( <div> {filtered.map(location => ( <div key={location.name}>{location.name}</div> ))} </div> ) }) export function LocationSearch() { const [searchValue, setSearchValue] = useState('') const deferredSearchValue = useDeferredValue(searchValue) return ( <> <input placeholder="Type here..." value={searchValue} onChange={e => setSearchValue(e.target.value)} /> <SearchResultList searchValue={deferredSearchValue} /> </> ) }
useDeferredValue sorgt dafür, dass die SearchResultList mit einem verzögerten Wert gerendert wird. React behandelt diesen „deferred“ Wert mit niedrigerer Priorität, sodass Eingaben weiterhin sofort verarbeitet werden, während das aufwendigere Rendern nachgelagert erfolgt. Auf Stackblitz habe ich zwei Beispiele gebaut, mit denen sich deferred und normale Suchfilter direkt vergleichen lassen.
Manchmal betrifft eine Nutzeraktion nicht nur einen einzelnen Wert, sondern stößt mehrere State-Updates und Re-Renders gleichzeitig an – etwa beim Anwenden eines Filters, der mehrere große Tabellen oder Diagramme neu berechnet. In solchen Fällen kann useTransition genutzt werden, um diese Updates als nicht dringend zu markieren.
import { useState, useTransition } from 'react' import type { Report } from '../types' const allReports: Report[] = [ { id: 'rep-001', category: 'sales', title: 'Monthly Revenue', amount: 124_000, currency: 'EUR', updatedAt: '2026-01-15', }, // hundreds more ... ] export default function AnalyticsPanel() { const [filter, setFilter] = useState('') const [reports, setReports] = useState(allReports) const [isPending, startTransition] = useTransition() const handleFilterChange = (value: string) => { setFilter(value) startTransition(() => { const query = value.toLowerCase() const filteredReports = allReports.filter(report => report.category.toLowerCase().includes(query) ) setReports(filteredReports) }) } return ( <> <input placeholder="Filter by category..." value={filter} onChange={e => handleFilterChange(e.target.value)} /> {isPending && <div>Loading data...</div>} <div> {reports.map(report => ( <div key={report.id}> <strong>{report.category}</strong>: {report.amount} {report.currency} </div> ))} </div> </> ) }
Mit startTransition werden diese Updates bewusst mit niedriger Priorität ausgeführt. React hält dadurch Interaktionen wie Tippen, Scrollen oder Klicken responsiv, während die rechenintensiven Aktualisierungen im Hintergrund verarbeitet werden. Über isPending lässt sich erkennen, ob eine Transition noch läuft.
Ein weiterer hilfreicher Hook zur UI-Optimierung ist useOptimistic. Er implementiert das Konzept der Optimistic UI, bei dem Änderungen im UI sofort sichtbar werden, während die eigentliche Serveranfrage im Hintergrund verarbeitet wird. Gerade bei Likes oder Kommentaren fühlt sich die App dadurch spürbar schneller an.
import { useState, useOptimistic, startTransition } from 'react' import { getId, sendMessageToServer } from './utils/fake-api' export interface Message { id: string text: string } export default function ChatApp() { const [messages, setMessages] = useState<Message[]>([]) const [input, setInput] = useState('') const [optimisticMessages, addOptimisticMessage] = useOptimistic< Message[], Message >(messages, (state, message) => [message, ...state]) const sendMessage = async () => { const text = input.trim() if (!text) { return } startTransition(async () => { addOptimisticMessage({ id: `generated-${getId()}`, text, }) const sentMessage = await sendMessageToServer(text) setMessages([sentMessage, ...messages]) }) setInput('') } return ( <div> <form className="chat-input-container" action={sendMessage}> <input className="chat-input" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Write a message…" /> <button className="chat-send-button" type="submit"> Send </button> </form> <div className="chat-messages"> {optimisticMessages.map(msg => ( <div className="message optimistic" key={msg.id}> {msg.text} </div> ))} </div> </div> ) }
addOptimistic zeigt eine Nachricht sofort im UI, noch bevor der Server antwortet. Jede Nachricht erhält dafür eine temporäre ID, damit React sie korrekt rendern kann. Sobald die Serverantwort kommt, wird die Nachricht durch die endgültige ersetzt. Bei einem Fehler wird die Änderung zurückgesetzt.
Auf Stackblitz habe ich zwei Beispiele gebaut, die den Unterschied zeigen: Mit useOptimistic erscheinen neue Nachrichten sofort im UI, noch während die Serveranfrage läuft, während sie ohne useOptimistic erst nach der Serverantwort sichtbar werden, was zu einer spürbaren Verzögerung führt.
Im Kern bedeutet das: useDeferredValue und useTransition sind dann sinnvoll, wenn größere Datenmengen verarbeitet oder Updates spürbar teuer werden. Bei kleinen Daten oder einfachen Berechnungen bringen sie kaum Vorteile und erhöhen nur die Komplexität. useOptimistic lohnt sich, wenn UI-Feedback reaktiv und flüssig sein soll, also Änderungen bereits im UI sichtbar sind, noch während die Serveranfrage läuft.
Alle drei Hooks greifen in das Rendering-Verhalten von React ein, verfolgen jedoch unterschiedliche Ziele. Die folgende Tabelle zeigt, welches Problem jeder Hook adressiert und in welchen Fällen sein Einsatz sinnvoll ist:
| Hook | Problem | Typische Use Case | Wann eher nicht |
|---|---|---|---|
| useDeferredValue | Input fühlt sich langsam an | Suche, Filter, Autocomplete, große Listen | Kleine Datenmengen |
| useTransition | UI friert bei komplexen Updates | Tabs, Navigation, Sortierungen, große State-Updates | Einfache State-Änderungen |
| useOptimistic | Nutzer erwarten ein flüssiges, reaktionsschnelles UI | Likes, Chats, Voting | Kritische Aktionen, keine Rollback-Möglichkeit |
Leidenschaftliche Softwareentwicklerin mit Fokus auf sauberen, wartbaren Code und benutzerfreundliche Oberflächen in ReactJS, TypeScript, Node.js, Java und Spring Boot - bereit, Ihr nächstes Projekt erfolgreich umzusetzen.
Superlative GmbH
Auf den Flachsbeckwiesen 19
45659 Recklinghausen
Deutschland
Geschäftsführer: Anton Bessonov
Verantwortlicher i.S.d. § 18 Abs. 2 MStV:
Anton Bessonov
Auf den Flachsbeckwiesen 19
45659 Recklinghausen
Registergericht: Amstgericht Recklinghausen
Registernummer: HRB 9109
Umsatzsteuer-Identifikationsnummer: DE351968265
Mitglied der Initiative "Fairness im Handel".
Nähere Informationen: https://www.fairness-im-handel.de