useEffect()
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.
Inhaltsverzeichnis
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.
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.
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.
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.
useEffect mit leeren Abhängigkeiten - Einmal und nie wieder
Die häufigste Form der Verwendung von useEffect()
sieht schematisch so aus.
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.
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.
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.
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.
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.
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.
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.
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.
useEffect(() => {
console.log('Läuft nach jedem Render-Vorgang');
});
Mit leeren Abhängigkeiten
In diesem Fall läuft die Funktion nur einmal.
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.
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();
}, []);