Der React useEffect Hook ermöglicht die Ausführung von Seiteneffekten in funktionalen Komponenten. Er dient als Ersatz für Lifecycle-Methoden wie componentDidMount, componentDidUpdate und componentWillUnmount in Klassenkomponenten. Mit useEffect lassen sich Daten fetchen, DOM-Manipulationen durchführen, Event-Listener registrieren und weitere asynchrone Operationen nach dem Rendering steuern. Die Dependency-Array-Funktion bietet präzise Kontrolle darüber, wann Effekte ausgeführt werden sollen.

Was ist useEffect()

Die Haupt-Aufgabe eines React-Components ist das Anzeigen von Daten oder Präsentation der UI. Manchmal möchte man allerdings bestimmte Aktionen ausführen, die in der ersten Linie nicht direkt mit dem Anzeigen der Elemente auf dem Bildschirm zu tun haben, sondern mit dem “Leben” dieser Komponente. Solche aktionen nennt man “Side Effects” (Nebenwirkungen).

Beispiele für Side Effects:

  • Daten von einem Server abrufen. Wenn ein Component Daten anzeigen soll, müssen diese irgendwoher kommen. Man ruft sie ab, sobald das Component initialisiert wurde.
  • Timer starten oder stoppen. Vielleicht soll ein Countdown laufen, sobald ein Component sichtbar ist.
  • Auf Ereignisse außerhalb von React reagieren. Zum Beispiel, wenn sich die Größe des Browserfensters ändert.

useEffect() ist wie ein Helfer, der bestimmte Aufgabe erledigt:

  • wenn das Component geladen wird

  • wenn State oder Props sich ändern

  • wenn das Component zerstört wird

    Hinweis zu Beispielen

    Die Beispiele wurden so aufgebaut, dass man immer noch ein Hilfs-Component hat, um das Einbinden des tatsächlichen Components steuern zu können. So kann man alle Seiten von useEffect() beleuchten.

Aufbau von useEffect - Einfachste Form

Die Einfachste Form von useEffect() sieht wie folgt aus.

TypeScript Schematisches Beispiel
import { useEffect } from 'react';

function MyComponent() {
    useEffect(() => {
        console.log('Ich werde immer ausgeführt!');
    });

    return (
        <div>
            <span>Hallo React</span>
        </div>
    );
}

Was passiert in diesem Code?

Wir übergeben useEffect() eine Funktion. Diese Funktion enthält den Code, der den “Side Effect” darstellt.

Wenn wir useEffect() mit nur einem Argument (der Funktion) und ohne das zweite Argument (keine leeren oder gefüllte eckigen Klammern), dann wird die Funktion, die man übergeben hat, nach dem jedem Render-Vorgang des Components ausgeführt.

Wann ist das nützlich?

Selten! Das ist in den meisten Fällen, was man nicht möchte, da es sehr ineffizient sein kann. Stellen wir uns vor, man würde jedes Mal, wenn jemand auf einen Button klickt, Daten vom Server abrufen. Das wäre Quatsch.

Beispiel

In diesem Beispiel werden wir useEffect() in der einfachsten Ausführung verwenden.

TypeScript UseEffect.jsx
import { useEffect, useState } from 'react';

function UseEffectExample() {
    const [name, setName] = useState('');

    useEffect(() => {
        console.log('Komponente wurde gerendert');
        document.title = `Hi ${name || 'Unbekannt'}`;
    });

    return (
        <div className="example-one">
            <h2>useEffect - Beispiel (1)</h2>
            <hr/>
            <label>Wie ist dein Name?</label>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Dein Name ..."
            />
        </div>
    );
}

// Hilfs-Component mit Button zum Steuern der Einbindung
function UseEffectController() {
    const [componentEnabled, setComponentEnabled] = useState(false);

    const handleToggleComponent = () => {
        setComponentEnabled(currentState => !currentState);
    };

    return (
        <div className="component-controller">
            <button onClick={handleToggleComponent}>
                Komponente {componentEnabled ? 'löschen' : 'einbinden'}
            </button>
            <hr/>
            {componentEnabled && (
                <UseEffectExample />
            )}
        </div>
    );
}

export default UseEffectController;

Wenn man zum ersten Mal auf den Button Komponente einbinden klickt, sieht man in der Konsole, dass useEffect() ausgeführt wird.

React Hooks - useEffect - Bild zeigt Ausführung beim Einbinden des Components

Wenn wir nun mit der Eingabe von Text am Eingabefeld starten, wird nach jedem Buchstaben bzw. nach jede Änderung des Wertes im Eingabefeld die Funktion erneut ausgeführt.

React Hooks - useEffect - Bild zeigt Ausführung nach Änderung von Daten

useEffect mit leeren Abhängigkeiten - Einmal und nie wieder

Die häufigste Form der Verwendung von useEffect() sieht schematisch so aus.

TypeScript Schematisches Beispiel
useEffect(() => {
    // Simulation
    setTimeout(() => {
        setUsername(current => 'User data here');
    }, 2000);
}, []);

Was passiert nun in diesem Code?

Wir übergeben wieder eine Funktion, aber diesmal als zweites Argument übergeben wir leere eckige Klammern (leeres Array []).

Was hat das leere Array zu bedeuten? Es bedeutet: Führe diese Funktion nur einmalig aus, nachdem das Component das erste Mal auf den Bildschirm geladen wurde (gemountet wurde).

Wann ist das nützlich?

Genau dann, wenn man Dinge tun möchte, die einmalig bei der Initialisierung des Components passieren sollen.

  • Daten vom Server abrufen
  • Initialisierung einer externen Bibliothek
  • Event Listener hinzufügen, welcher für die gesamte Lebensdauer des Components bestehen bleiben soll

Beispiel

In diesem Beispiel werden wir einen Zähler über State erhöhen.

Im Beispiel oben hat man gesehen, dass die Verwendung von useEffect() ohne des zweiten Parameters die Funktion immer ausführen lässt. In diesem Beispiel übergeben wir das zweite Argument als ein leeres Array, also ohne Abhängigkeiten.

TypeScript UseEffect.jsx
function UseEffectExample() {
    const [counter, setCounter] = useState(0);

    const handleCounterUpdate = () => {
        setCounter(currentCounter => currentCounter + 1);
    };

    useEffect(() => {
        console.log('Wird nur am Anfang ausgeführt');
    }, []);

    return (
        <>
            <p>Aktueller Zähler: {counter}</p>
            <button onClick={handleCounterUpdate}>
                Zähler erhöhen
            </button>
        </>
    );
}

function UseEffectController() {
    const [componentEnabled, setComponentEnabled] = useState(false);

    const handleToggleComponent = () => {
        setComponentEnabled(currentState => !currentState);
    };

    return (
        <div className="component-controller">
            <button onClick={handleToggleComponent}>
                Komponente {componentEnabled ? 'löschen' : 'einbinden'}
            </button>
            <hr/>
            {componentEnabled && (
                <UseEffectExample />
            )}
        </div>
    );
}

export default UseEffectController;

Die Funktion useEffect führt die übergebene Side Effect Funktion nur einmalig zu Beginn aus. Auch wenn man den Zähler erhöht, wird die Funktion nicht ein weiteres Mal ausgeführt.

React Hooks - useEffect - Beispiel für Verwendung mit leeren Abhängigkeiten

useEffect() mit Abhängigkeiten - Wenn sich etwas ändert

Ab diesem moment wird useEffect() mächtiger. Man kann useEffect() mitteilen, dass es den Side Effect (die übergebene Funktion) nur dann ausführen soll, wenn sich bestimmte Werte ändern.

Und die Werte, welche auf Änderung überwacht werden sollen, werden in das Array, das als das zweite Argument übergeben wird, hineingegeben.

Schematische Verwendung in diesem Fall würde so aussehen.

TypeScript Schematisches Beispiel
const [counter, setCounter] = useState(0);

useEffect(() => {
    console.log('Zähler hat sich geändert:', counter);
}, [counter]);

Ok, was passiert nun in diesem schematischen Beispiel.

Durch die Platzierung der Variable ‘counter’ aus unserer Zustandsverwaltung, haben wir eine Abhängigkeit übergeben. Das bedeutet, dass der Side Effect, also die übergebene Funktion nur ausgeführt wird, wenn sich der Wert von ‘counter’ ändert.

Wenn sich der Wert der Variable ‘message’ ändert, aber der ‘counter’ gleich bleibt, wird der Effekt nicht ausgeführt.

Dadurch hat man eine abgestimmte Ausführung von useEffect(). Eine Ausführung findet nur dann statt, wenn diese, aufgrund der Änderung von Abhängigkeitswerten, notwendig ist.

Wann ist es nützlich?

  • Wenn man Daten neu abrufen möchte, basierend auf einer Benutzeraktion oder einer Änderung in den Props.
  • Einen Timer zurücksetzen, wenn sich ein bestimmter Wert ändert.
  • Animationen neu starten, wenn sich ein Zustand ändert.

Beispiel

In diesem Beispiel werden wir das Laden von unterschiedlichen Benutzern simulieren. Dabei verwenden wir useState() und auch useEffect() mit einer Abhängigkeit. Das Component stellt eine Select-Liste bereit, in der man einen Benutzer auswählen kann, welcher geladen werden soll.

TypeScript UseEffect.jsx
function UseEffectExampleThree() {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [userId, setUserId] = useState(1);

    useEffect(() => {
        console.log(`Lade Benutzer mit ID: ${userId}`);
        setLoading(true);

        const timeout = setTimeout(() => {
            const fakeUsers = {
                1: { name: 'John', email: 'john@mail.com', job: 'Developer' },
                2: { name: 'Tom', email: 'tom@mail.com', job: 'Designer' },
                3: { name: 'Lisa', email: 'lisa@mail.com', job: 'Manager' }
            };

            setUser(fakeUsers[userId] || null);
            setLoading(false);
        }, 1000);

        return () => {
            clearTimeout(timeout);
            console.log('Cleanup: Alte Anfrage abgebrochen');
        };
    }, [userId]);

    return (
        <div className="user-list">
            <label>Benutzer auswählen</label>
            <select
                value={userId}
                onChange={(e) => setUserId(current => Number(e.target.value))}
            >
                <option value={1}>Benutzer 1</option>
                <option value={2}>Benutzer 2</option>
                <option value={3}>Benutzer 3</option>
            </select>

            <hr/>

            {loading ? (
                <div className="loader">Lade Benutzer ...</div>
            ) : user ? (
                <div className="user-info">
                    <h3>{user.name}</h3>
                    <p>{user.email}</p>
                    <p>{user.job}</p>
                </div>
            ) : (
                <div className="not-found-info">
                    <p>Benutzer nicht gefunden</p>
                </div>
            )}
        </div>
    );
}

function UseEffectController() {
    const [componentEnabled, setComponentEnabled] = useState(false);

    const handleToggleComponent = () => {
        setComponentEnabled(currentState => !currentState);
    };

    return (
        <div className="component-controller">
            <button onClick={handleToggleComponent}>
                Komponente {componentEnabled ? 'löschen' : 'einbinden'}
            </button>
            <hr/>
            {componentEnabled && (
                <UseEffectExampleThree />
            )}
        </div>
    );
}

Wenn wir dieses Beispiel initial im Browser laden, erhalten wir folgenden Zustand.

React Hooks - useEffect - Beispiel - Verwendung mit Abhängigkeit 1

Wählt man einen anderen Benutzer aus, wird der change Event am Select-Element ausgelöst und ein anderer Benutzer geladen. Dabei wird useEffect() erneut ausgeführt, da wir dort als Abhängigkeit userId angegeben haben. Sprich, immer wenn sich diese Ändert, wird die Side Effect Funktion in useEffect() ausgeführt.

Einen weiteren Effekt sieht man beim Löschen (Aushängen) des Components. In diesem Fall wird die Rückgabe-Funktion aus der useEffect() Funktion ausgeführt.

React Hooks - useEffect - Beispiel - Verwendung mit Abhängigkeit 2

Cleanup Mechanismus von useEffect()

Manchmal muss man nach einem Side Effect aufräumen, bevor das Component verschwindet oder der Effekt erneut ausgeführt wird. Das ist sehr wichtig, um Speicherlecks oder unerwartetes Verhalten zu vermeiden.

Das kann man tun, indem man eine Aufräum-Funktion aus useEffect() zurückgibt.

TypeScript Schematisches Beispiel
useEffect(() => {
    // Side Effect Code

    // Wird ausgeführt, wenn das Component verschwindet,
    // oder der Effekt neu ausgeführt wird.
    return () => {
        console.log('Do something');
    };
}, []);

Drei Varianten - Zusammenfassung

Fassen wir nochmals die drei unterschiedliche Ausführungsvarianten von useEffect() zusammen.

Ohne Abhängigkeiten

Hier läuft die Funktion immer.

TypeScript Ohne Abhängigkeiten
useEffect(() => {
    console.log('Läuft nach jedem Render-Vorgang');
});

Mit leeren Abhängigkeiten

In diesem Fall läuft die Funktion nur einmal.

TypeScript Ohne Abhängigkeiten
useEffect(() => {
    console.log('Läuft nur beim ersten Mal');
}, []);

Mit Abhängigkeiten

Bei Vorhandensein von Abhängigkeiten läuft die Funktion nur dann, wenn sich bestimmte Werte ändern.

TypeScript Ohne Abhängigkeiten
useEffect(() => {
    console.log('Läuft nur, wenn sich "name" ändert');
}, [name]);

Wichtigste Regeln - Zusammenfassung

Dependency Array ist entscheidend

❌ Schlecht: Läuft nach jedem Render

useEffect(() => {
    console.log('Läuft zu oft!');
});

✅ Gut: Läuft nur einmal

useEffect(() => {
    console.log('Läuft nur beim Start');
}, []);

✅ Gut: Läuft nur, wenn sich ‘name’ ändert

useEffect(() => {
    console.log('Name hat sich geändert:', name);
}, [name]);

Alle verwendeten Variablen gehören in die Dependencies

❌ Schlecht: ‘alter’ wird verwendet, aber nicht in Dependencies

function MyComponent() {
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);

    useEffect(() => {
        console.log(`${name} ist ${age} Jahre alt`);
    }, [name]);
}

✅ Gut: Alle verwendeten Variablen sind in Dependencies

function MyComponent() {
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);

    useEffect(() => {
        console.log(`${name} ist ${age} Jahre alt`);
    }, [name, age]);
}

Cleanup nicht vergessen

❌ Schlecht: Timer läuft auch nach Component-Entfernung weiter

useEffect(() => {
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);
}, []);

✅ Gut: Timer wird ordentlich gestoppt

useEffect(() => {
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);

    return () => clearInterval(timer);
}, []);

Häufige Fehler und Lösungen

Unendliche Schleifen

❌ Das führt zu einer Endlosschleife

function MyComponent() {
    const [data, setData] = useState([]);

    useEffect(() => {
        setData([...data, 'New entry']);
    }, [data]);
}

Die Daten werden hier innerhalb von useEffect() geändert, was sofort dazu führt, dass useEffect() wieder ausgeführt wird.

Async/await direkt in useEffect

❌ Schlecht: useEffect kann nicht async sein

useEffect(async() => {
    const response = await fetch('api/data');
}, []);

✅ Gut: Async-Funktion innerhalb von useEffect

useEffect(() => {
    const loadData = async () => {
        const response = await fetch('api/data');
        const data = await response.json();
        setData(data);
    };

    loadData();
}, []);
/ Weiter

Zurück zu Hooks

Zur Übersicht