Menu

React useCallback Hook: Wann er wirklich hilft

useCallback gibt zwischen Renders dieselbe Funktion zurück, bis sich ihre Abhängigkeiten ändern. Das hilft nur, wenn diese Funktion an ein Kind mit memo übergeben oder als Abhängigkeit eines Effekts genutzt wird. Lerne die Syntax, den Unterschied zu useMemo und die Fehler, die du vermeiden solltest.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

useCallback cacht eine Funktion zwischen Renders: Es gibt dasselbe Funktionsobjekt zurück, bis sich eine seiner Abhängigkeiten ändert. Das ist nur wichtig, wenn etwas die Funktion per Referenz vergleicht, in der Praxis also ein in memo gepacktes Kind oder ein Effekt, der die Funktion als Abhängigkeit führt.

Beide Buttons sind memoisiert, und beide tun dasselbe. Schalte das Theme um und beobachte die Konsole: Nur render Inline function erscheint, weil saveInline bei jedem Render eine neue Funktion ist, also sieht memo eine geänderte Prop. saveCached ist jedes Mal dieselbe Funktion, also wird dieser Button übersprungen.

Was es cacht

const cachedFn = useCallback(fn, dependencies);
  • fn ist die Funktion, die du behalten willst. React ruft sie nicht auf; es gibt sie dir zurück.
  • dependencies listet jeden Wert aus der Komponente, den fn liest. Bei jedem Render vergleicht React sie per Object.is mit den vorherigen.
  • Hat sich nichts geändert, bekommst du die Funktion aus dem vorherigen Render. Hat sich etwas geändert, bekommst du die Funktion, die du diesmal übergeben hast, und React behält diese für das nächste Mal.

Beachte, was das bedeutet: Die Arrow Function in useCallback wird trotzdem bei jedem Render erzeugt, weil sie nur ein Argument ist. useCallback spart diese Arbeit nicht. Seine einzige Aufgabe ist, eine ältere, identische Funktion zurückzugeben, damit die Referenz gleich bleibt.

useCallback vs. useMemo

Es ist derselbe Mechanismus. useMemo cacht das Ergebnis eines Funktionsaufrufs, useCallback die Funktion selbst:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Nutze useCallback für Funktionen, weil es sich besser liest, und useMemo für berechnete Werte wie eine gefilterte Liste oder ein Objekt mit Optionen.

Abhängigkeiten mit Updater-Funktionen loswerden

Eine gecachte Funktion muss jeden State-Wert auflisten, den sie liest, und jedes Mal, wenn sich dieser State ändert, ändert sich auch die Funktion. Bei einer Liste memoisierter Zeilen ist das ein Problem: Eine Funktion remove, die todos liest, ändert sich, wann immer sich todos ändert, also rendert jede verbleibende Zeile nach jedem Entfernen erneut. Nutze die Updater-Form des Setters, damit die Funktion todos gar nicht mehr liest.

Entferne eine Zeile: Die Konsole bleibt still, weil die anderen Zeilen dasselbe Objekt todo und dieselbe Funktion remove wie vorher bekommen haben. Ändere die Funktion zu useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) und versuche es erneut: Jetzt loggt jede verbleibende Zeile nach jedem Entfernen einen Render.

Funktionen als Abhängigkeiten von Effekten

Ein Effekt, der eine in der Komponente deklarierte Funktion aufruft, muss diese Funktion als Abhängigkeit führen. Wenn die Funktion bei jedem Render neu ist, läuft der Effekt nach jedem Render. useCallback hält sie stabil, bis sich ihre eigenen Eingaben ändern:

Eine Nachricht zu tippen verbindet nicht neu. Ein Wechsel des Raums loggt ein Trennen vom alten Raum und ein Verbinden mit dem neuen. Lösch den Wrapper useCallback (die Arrow Function bleibt), und jeder Tastendruck trennt und verbindet neu.

Hier gibt es allerdings eine einfachere Lösung: Verschiebe die Funktion in den Effekt. Dann hängt der Effekt direkt von roomId ab, und keine Funktion muss stabil sein.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Bevorzuge das, wenn nur der Effekt die Funktion nutzt. Behalte useCallback für Funktionen, die du auch nach unten reichst oder aus einem Custom Hook zurückgibst, wo Aufrufer sie in ihre eigenen Abhängigkeitsarrays setzen könnten.

Prüfen, ob sich eine Funktion geändert hat

Wenn ein Kind mit memo ständig rendert und du nicht sicher bist, warum, prüfe, ob die Funktions-Prop zwischen Renders wirklich dasselbe Objekt ist. Halte die vorherige in einer Ref und vergleiche:

Klicke auf Count: Die Konsole sagt increment changed: false, weil sich step nicht geändert hat. Klicke auf Step, und sie sagt true: step ist eine Abhängigkeit, also hat React die neue Funktion zurückgegeben. Eine Funktion, die sich bei jedem Render ändert, bedeutet meist, dass eine Abhängigkeit selbst bei jedem Render neu ist, etwa ein Objekt oder eine andere in der Komponente deklarierte Funktion.

useCallback in Custom Hooks

Ein Custom Hook, der Funktionen zurückgibt, ist ein guter Ort für useCallback, selbst wenn du nicht weißt, wer ihn aufrufen wird. Die Komponente, die den Hook nutzt, übergibt diese Funktionen vielleicht an ein Kind mit memo oder führt sie in einem Effekt auf und kann sie von außen nicht stabil machen. Sie im Hook zu umhüllen gibt jedem Aufrufer standardmäßig eine stabile Referenz:

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

Der häufige Fehler: alles umhüllen

useCallback um einen Handler, der direkt an ein <button> oder <input> geht, bringt nichts Nützliches. React überspringt bei einem DOM-Element keine Arbeit, nur weil sein Handler gleich geblieben ist, und eine nicht in memo gepackte Komponente rendert ohnehin, wenn ihr Elternteil rendert.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Jedes zusätzliche useCallback bringt ein Abhängigkeitsarray mit, das man falsch machen kann, und eine veraltete Abhängigkeit ist ein echter Bug (die Funktion liest weiter alten State). Füge es hinzu, wenn auf der empfangenden Seite ein Kind mit memo oder ein Abhängigkeitsarray steht, nicht standardmäßig.

React Compiler 1.0 memoisiert, wenn dein Build ihn nutzt, Funktionen und Werte automatisch, was die meisten von Hand geschriebenen useCallback-Aufrufe überflüssig macht. Ohne Compiler gelten die Regeln oben weiterhin, und manuelles useCallback ist in bestehendem Code weiterhin üblich.

Häufig gestellte Fragen

Was macht useCallback?

Es gibt bei jedem Render dasselbe Funktionsobjekt zurück, bis sich eine seiner Abhängigkeiten ändert. Es verhindert nicht, dass die Funktion erzeugt wird; es gibt statt der neuen die Funktion aus einem früheren Render zurück.

Wann sollte ich useCallback nutzen?

Wenn du eine Funktion an ein in memo gepacktes Kind übergibst oder wenn die Funktion eine Abhängigkeit eines Effekts oder eines anderen Hooks ist. In beiden Fällen würde eine neue Funktion bei jedem Render die Optimierung aushebeln oder den Effekt erneut ausführen.

Was ist der Unterschied zwischen useCallback und useMemo?

useCallback(fn, deps) cacht fn selbst. useMemo(() => value, deps) cacht den Wert, den eine Funktion zurückgibt. useCallback(fn, deps) ist genau useMemo(() => fn, deps).

Sollte ich jede Funktion in useCallback packen?

Nein. Wenn die Funktion an ein einfaches DOM-Element oder eine nicht memoisierte Komponente geht, ändert eine stabile Referenz nichts, und die zusätzlichen Abhängigkeitsarrays machen den Code schwerer lesbar und fehleranfälliger.

Macht useCallback meine Komponente schneller?

Nicht allein. Es hilft nur, wenn etwas weiter unten die Funktion per Referenz vergleicht, etwa memo oder das Abhängigkeitsarray eines Effekts.

Illustration der Programmiersprachen bei Coddy

Lerne mit Coddy zu programmieren

LOS GEHT'S