useTransition (React 18+) markiert einen State-Update als nicht-dringend. React darf den damit verbundenen Re-Render unterbrechen, wenn dringendere Updates eintreffen — Tastendrücke, Maus-Events, andere Interaktionen. Die UI bleibt responsiv, auch wenn der State-Wechsel intern ein teures Re-Rendering auslöst. Klassisches Beispiel: ein Suchfeld, dessen Eingabe sofort sichtbar ist, während das Filtern einer großen Liste im Hintergrund läuft. Der Hook liefert zwei Werte: ein isPending-Flag (für Loading-Anzeigen) und eine startTransition-Funktion, in deren Callback man die nicht-dringenden Setter aufruft.

Signatur

TypeScript signatur.ts
const [isPending, startTransition] = useTransition();

startTransition(callback) markiert alle State-Updates im Callback als Transition. isPending ist true, solange die Transition läuft.

Klassisches Beispiel — Such-Filter

TypeScript SearchFilter.jsx
import { useState, useTransition } from 'react';

export default function SearchFilter({ items }) {
    const [query, setQuery] = useState('');
    const [filtered, setFiltered] = useState(items);
    const [isPending, startTransition] = useTransition();

    const handleChange = (e) => {
        const value = e.target.value;
        setQuery(value);   // dringend — Input zeigt Eingabe sofort

        startTransition(() => {
            // nicht dringend — teures Filtern
            setFiltered(items.filter(i => i.name.includes(value)));
        });
    };

    return (
        <>
            <input value={query} onChange={handleChange} />
            {isPending && <Spinner />}
            <ul>
                {filtered.map(i => <li key={i.id}>{i.name}</li>)}
            </ul>
        </>
    );
}

Ohne useTransition: jede Eingabe blockiert die UI, bis das Filtern fertig ist. Mit useTransition: Eingabe ist sofort sichtbar, das Filter-Ergebnis kommt nach — und falls der User schnell weiter tippt, verwirft React die Zwischen-Renders.

startTransition ohne Hook

Es gibt auch eine Module-Level-Variante import { startTransition } from 'react', die ohne Hook auskommt — aber kein isPending liefert.

TypeScript StandaloneStartTransition.jsx
import { startTransition } from 'react';

function navigate(url) {
    startTransition(() => {
        router.push(url);   // nicht dringend
    });
}

Nützlich außerhalb von Components (in Router-Libraries, Event-Handlern). Wer isPending braucht: den Hook nutzen.

Was darf in startTransition?

Erlaubt: synchrone State-Setter (setState(...), dispatch(...)).

NICHT erlaubt: async-Operationen wie await direkt im Callback. Promises müssen im Callback erstellt und ihre Resolves dann manuell in einer weiteren startTransition setzen.

TypeScript Erlaubt.jsx
// RICHTIG — synchrone Updates
startTransition(() => {
    setQuery(value);
    setPage(1);
    setFiltered(filterData(value));
});

// FALSCH — async-Callback wird nicht als Transition behandelt
startTransition(async () => {
    await fetch(url);   // dieser Setter ist nicht in der Transition
    setData(await response.json());
});

// KORREKT bei async — Setter explizit in einer zweiten startTransition
const handleClick = async () => {
    const data = await fetch(url).then(r => r.json());
    startTransition(() => {
        setData(data);
    });
};

Wann useTransition?

Sinnvoll, wenn:

  • ein State-Update einen spürbar teuren Re-Render auslöst (große Listen, komplexe Tabellen, Charts).
  • der User währenddessen weiter mit der UI interagieren können soll (tippen, scrollen).
  • ein Loading-State während des langen Updates angezeigt werden soll.

NICHT sinnvoll:

  • bei einfachen Updates ohne wahrnehmbare Verzögerung.
  • bei kritischen Updates, die sofort sichtbar sein müssen.
  • als Standard-Optimierung „überall einbauen" — nur dort, wo Performance-Probleme gemessen wurden.

useDeferredValue als Schwesternsystem

useDeferredValue macht etwas Ähnliches, aber für Werte statt Updates: man bekommt eine „verzögerte" Kopie eines Werts, die hinterhereilt, wenn der direkte Wert hochfrequent updatet. Details im eigenen Artikel.

Besonderheiten

useTransition verändert die Reihenfolge, nicht das Endergebnis.

Alle Setter laufen am Ende durch — Transition macht sie nur „unterbrechbar" und priorisiert dringende Updates dazwischen. Der State ist nach Abschluss identisch zu ohne Transition.

isPending ist true zwischen Start und Commit der Transition.

Während isPending aktiv ist, läuft React am Transition-Render. Sobald er committet ist, wird isPending false. Praktisch für Spinner/Disable-Logik.

Dringende Updates IMMER außerhalb von startTransition.

Der Setter für den Input-Wert MUSS sofort sichtbar sein — sonst wirkt das Feld eingefroren. Daher: nur den teuren Folge-Update in die Transition packen.

Mehrere startTransition werden gebatcht.

Setter aus verschiedenen startTransition-Aufrufen im selben Event werden zusammengefasst. Genau ein Transition-Render am Ende.

async/`await` direkt im Callback funktioniert NICHT.

Nur die SYNCHRONEN Setter im Callback werden als Transition markiert. Nach await sind alle Setter wieder dringend. Lösung: nach dem await EXPLIZIT startTransition erneut aufrufen.

Browser-DevTools Performance: Transitions sind im Profile sichtbar.

React DevTools markieren Transition-Renders in der Timeline. Hilft beim Verifizieren, dass die Transition wirklich greift und nicht versehentlich „dringend" geworden ist.

useTransition ist Concurrent-Feature — benötigt React 18+.

Vor 18 gab es keine Concurrent-Mode-API. Apps, die noch auf React 17 sind, müssen ohne auskommen — oder manuell mit setTimeout/requestIdleCallback simulieren (deutlich schlechter).

React Router 6.4+ nutzt Transitions intern für Navigation.

Beim Route-Wechsel läuft die neue Seite als Transition. useNavigation() liefert dort einen state, der dem isPending hier entspricht. Genau das Pattern für „Spinner während Page-Wechsel".

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Hooks

Zur Übersicht