navigation Navigation


useCallback()


Der useCallback Hook gehört zu den Optimierungs-Hooks in React und dient der Leistungsverbesserung durch Memoization von Callback-Funktionen. Diese Technik verhindert unnötige Neuberechnungen und Rendervorgänge bei jedem Rendering-Zyklus. Mit useCallback werden Funktionsreferenzen zwischen Rendervorgängen beibehalten, solange sich die definierten Abhängigkeiten nicht ändern. Der Hook ist besonders nützlich beim Arbeiten mit Komponenten, die auf Referenzgleichheit prüfen oder PureComponents verwenden, sowie bei der Übergabe von Callbacks an stark optimierte untergeordnete Komponenten.

Inhaltsverzeichnis

    Einführung

    In React werden Funktionen standardmäßig bei jedem Rendern einer Komponente neu erzeugt. Das führt oft zu unnötigen Neuberechnungen oder zu unerwünschtem Verhalten.

    So wird, beispielsweise, eine Kind-Komponente, an die man eine Funktion als Prop weitergibt, bei jedem Render der Eltern-Komponente ebenfalls neu gerendert, weil die Funktion immer eine neue Referenz hat. Und da React die Referenzen überwacht, löst es ein Re-Rendering aus.

    In Kombination mit useEffect() oder React.memo kann das zu Performanceproblemen oder Endlosschleifen führen.

    useCallback() verhindert das, indem es die Funktion nur dann neu erzeugt, wenn sich eine oder mehrere Abhängigkeiten geändert haben.

    Syntax

    Die Syntax, oder besser gesagt, das Verwendungsschema, sieht wie folgt aus.

    const memoizedCallback = useCallback(() => {
        // Funktionaler Code hier
    }, [abhängigkeiten]);

    Parameter

    • Erster Parameter: Eine Funktion, die man memoisiert haben möchte.
    • Zweiter Parameter: Ein Array von Abhängigkeiten. Nur wenn sich eine dieser Abhängigkeiten ändert, wird die Funktion neu erstellt.

    Beispiel 1

    In diesem Beispiel wird es ein Eingabefeld geben, in das man Zahlen eingeben kann. Sobald eine neue Zahl im Eingabefeld vorhanden ist, wird die Liste der Begleitzahlen in der Kind-Komponente aktualisiert.

    Zusätzlich platzieren wir einen Button, mit dem man das Theme (dunkel/hell) umschalten kann.

    Ohne useCallback

    Zuerst bauen wir das Beispiel ohne Verwendung von useCallback() auf, um das Problem zu sehen.

    UseCallbackExampleOne
    import { useState } from 'react';
    
    function UseCallbackExampleOne() {
        const [counter, setCounter] = useState(0);
        const [theme, setTheme] = useState('light');
    
        const handleUpdateCounter = () => {
            setCounter(Number(e.target.value));
        };
    
        const handleUpdateTheme = () => {
            setTheme(currentTheme => currentTheme === 'light' ? 'dark' : 'light');
        };
    
        const getCalculatedNums = () => {
            return [counter, counter * 2, counter * 4];
        };
    
        return (
            <div style={{
                padding: 10,
                backgroundColor: theme === 'light' ? '#f1f1f1' : '#323232',
                color: theme === 'light' ? '#222222' : '#ffffff'
            }}>
                <div style={{
                    display: 'flex',
                    gap: 10,
                    alignItems: 'center',
                    justifyContent: 'center'
                }}>
                    <input
                        type="number"
                        value={counter}
                        onChange={handleUpdateCounter}
                    />
                    <button onClick={handleUpdateTheme}>
                        Toggle theme
                    </button>
                </div>
                <hr />
                <NumList nums={getCalculatedNums} />
            </div>
        );
    }
    
    function NumList({ nums }) {
        const [numItems, setNumItems] = useState([]);
    
        useEffect(() => {
            setNumItems(nums());
            console.log('Updating nums items');
        }, [nums]);
    
        return (
            <div
                style={{
                    display: 'flex',
                    flexDirection: 'column',
                    padding: 10,
                }}
            >
                {numItems.map((n, i) => (
                    <span key={i}>{n}</span>
                ))}
            </div>
        );
    }
    
    export default UseCallbackExampleOne;

    Was haben wir also in diesem Beispiel für einen Zustand? Wenn wir eine Zahl im Eingabefeld ändern, aktualisiert sich das Parent-Component (UseCallbackExampleOne). Gleichzeitig werden die Zahlen im Child-Component (NumList) ebenfalls aktualisiert. Das ist soweit ok.

    Klicken wir auf den Button “Toggle Theme”, stellen wir fest, dass das Child-Component (NumList) auch in diesem Fall aktualisiert hat, obwohl sich die Zahlen und counter gar nicht geändert haben.

    Warum passiert das? Durch die Änderung einer State-Eigenschaft wird das Component neu aufgebaut. Entsprechend werden auch die Funktionen neu erstellt. In unserem Beispiel wird dabei die Funktion getCalculatedNums() bei jedem Render neu erstellt. Sobald die Funktion neu erstellt wird, ändert sich auch die, an das Child-Component übergebene, Referenz (nums). Für React ist das ein Zeichen das Component neu zu rendern.

    Das heißt, wir haben hier eine überflüssige Aktualisierung des Child-Components beim Ändern des Themes.

    React Hook - useCallback - Beispiel 1 ohne useCallback


    Mit useCallback

    Als Lösung eilt hier useCallback() zur Hilfe. Die Funktion, welche die Zahlen-Liste berechnet und zurückgibt, wird mit useCallback() umhüllt und sorgt dafür, dass diese Funktion nur dann neu erstellt wird, wenn sich über übergebene Abhängigkeit counter ändert. Beim Ändern des Themes ist dies nicht der Fall. Entsprechend wird dadurch verhindert, dass diese Funktion neu erstellt und das Re-Rendering des Child-Components ausgeführt wird.

    UseCallbackExampleOne
    import { useState, useCallback } from 'react';
    
    function UseCallbackExampleOne() {
        const [counter, setCounter] = useState(0);
        const [theme, setTheme] = useState('light');
    
        const handleUpdateCounter = () => {
            setCounter(Number(e.target.value));
        };
    
        const handleUpdateTheme = () => {
            setTheme(currentTheme => currentTheme === 'light' ? 'dark' : 'light');
        };
    
        const getCalculatedNums = useCallback(() => {
            return [counter, counter * 2, counter * 4];
        }, [counter]);
    
        return (
            <div style={{
                padding: 10,
                backgroundColor: theme === 'light' ? '#f1f1f1' : '#323232',
                color: theme === 'light' ? '#222222' : '#ffffff'
            }}>
                <div style={{
                    display: 'flex',
                    gap: 10,
                    alignItems: 'center',
                    justifyContent: 'center'
                }}>
                    <input
                        type="number"
                        value={counter}
                        onChange={handleUpdateCounter}
                    />
                    <button onClick={handleUpdateTheme}>
                        Toggle theme
                    </button>
                </div>
                <hr />
                <NumList nums={getCalculatedNums} />
            </div>
        );
    }
    
    function NumList({ nums }) {
        const [numItems, setNumItems] = useState([]);
    
        useEffect(() => {
            setNumItems(nums());
            console.log('Updating nums items');
        }, [nums]);
    
        return (
            <div
                style={{
                    display: 'flex',
                    flexDirection: 'column',
                    padding: 10,
                }}
            >
                {numItems.map((n, i) => (
                    <span key={i}>{n}</span>
                ))}
            </div>
        );
    }
    
    export default UseCallbackExampleOne;

    In diesem Fall wird die Funktion getCalculatedNums() nur dann ausgeführt bzw. neu erstellt, wenn sich die Abhängigkeit counter ändert.

    React Hook - useCallback - Beispiel 1 mit useCallback

    Man kann allerdings diese Funktion (getCalculatedNums) auch manuell ausführen und dabei wird dennoch kein Re-Rendering von Child-Component stattfinden.

    Wir ergänzen das Beispiel um einen weiteren Button, welcher diese Funktion auf dem direkten Wege ausführt.

    UseCallbackExampleOne
    import { useState, useCallback } from 'react';
    
    function UseCallbackExampleOne() {
        const [counter, setCounter] = useState(0);
        const [theme, setTheme] = useState('light');
    
        const handleUpdateCounter = () => {
            setCounter(Number(e.target.value));
        };
    
        const handleUpdateTheme = () => {
            setTheme(currentTheme => currentTheme === 'light' ? 'dark' : 'light');
        };
    
        const getCalculatedNums = useCallback(() => {
            console.log('getCalculatedNums ausgeführt');
            return [counter, counter * 2, counter * 4];
        }, [counter]);
    
        return (
            <div style={{
                padding: 10,
                backgroundColor: theme === 'light' ? '#f1f1f1' : '#323232',
                color: theme === 'light' ? '#222222' : '#ffffff'
            }}>
                <div style={{
                    display: 'flex',
                    gap: 10,
                    alignItems: 'center',
                    justifyContent: 'center'
                }}>
                    <input
                        type="number"
                        value={counter}
                        onChange={handleUpdateCounter}
                    />
                    <button onClick={handleUpdateTheme}>
                        Toggle theme
                    </button>
                </div>
                <hr />
                <NumList nums={getCalculatedNums} />
                <hr />
                <button onClick={getCalculatedNums}>
                    getCalculatedNums ausführen
                </button>
            </div>
        );
    }
    
    function NumList({ nums }) {
        const [numItems, setNumItems] = useState([]);
    
        useEffect(() => {
            setNumItems(nums());
            console.log('Updating nums items');
        }, [nums]);
    
        return (
            <div
                style={{
                    display: 'flex',
                    flexDirection: 'column',
                    padding: 10,
                }}
            >
                {numItems.map((n, i) => (
                    <span key={i}>{n}</span>
                ))}
            </div>
        );
    }
    
    export default UseCallbackExampleOne;

    React Hook - useCallback - Manuelle Ausführung der Funktion

    Wie man hier sieht, wird durch den Klick auf den Button “getCalculatedNums ausführen” die Funktion getCalculatedNums() ausgeführt. Dabei wird die Funktion an sich aber nicht neu erstellt. Was entsprechenderweise zu keinem Re-Rendering des Child-Components führt.