useState und useEffect: richtig verwenden
1. Überblick
useState und useEffect sind die zwei grundlegendsten Hooks von React, aber auch die häufigste Quelle von Bugs: Komponenten rendern sich unendlich, State wird nicht wie erwartet aktualisiert, Effects laufen wiederholt ab, obwohl nicht nötig. Dieser Artikel behandelt die tatsächliche Funktionsweise dieser zwei Hooks, richtige Patterns und häufige Fehler, die du vermeiden solltest. Nach dem Lesen kannst du selbstbewusster die Frage beantworten „Brauche ich hier wirklich useEffect?” — eine Frage, die viele React-Entwickler immer noch falsch beantworten.
2. useState – Grundlagen
useState gibt ein Paar [value, setter] zurück. Jedes Mal, wenn man den setter aufruft, plant React ein Neurendern der Komponente mit dem neuen Wert ein.
import { useState } from "react";
function CustomerCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Kundenanzahl: {count}</p>
<button onClick={() => setCount(count + 1)}>Kunde hinzufügen</button>
</div>
);
}
useState(0)initialisiertcount = 0nur beim ersten Render.setCount(count + 1)erstellt einen neuen Render mitcounterhöht um 1.countin der Closure des aktuellen Renders ist unveränderbar — es ändert sich nicht automatisch zwischen zwei Renders.
Hinweis: Das Aufrufen von
setCountändert den Wert voncountnicht sofort im selben Render. React fasst die Updates zusammen und rendert danach neu.
3. useState: State-Updates basierend auf altem State
Wenn du den setter mehrfach im selben Event Handler basierend auf count aufrufen, wird das Ergebnis falsch.
function handleTripleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Ergebnis: count erhöht sich nur um 1, nicht um 3
}
Verwende functional updates, um den aktuellen Wert zu erhalten:
function handleTripleClick() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
// Ergebnis: count erhöht sich um genau 3
}
- Wenn du eine Funktion an den setter übergibst, stellt React sicher, dass
previmmer der neueste State-Wert ist, nicht der in der alten Closure erfasste Wert. - Regel: Wenn der neue State vom alten State abhängt, verwende immer functional updates.
4. useState: Lazy Initialization
Wenn der Initialisierungswert teuer zu berechnen ist, rufe die Funktion nicht direkt auf — übergebe eine Funktion an useState, damit sie nur einmal ausgeführt wird.
function parseInitialOrders(raw) {
console.log("Analysiere Order-Daten...");
return JSON.parse(raw);
}
function OrderList({ rawOrders }) {
// FALSCH: parseInitialOrders läuft bei jedem Render aus
// const [orders, setOrders] = useState(parseInitialOrders(rawOrders));
// RICHTIG: läuft nur beim ersten Render
const [orders, setOrders] = useState(() => parseInitialOrders(rawOrders));
return <p>Anzahl Orders: {orders.length}</p>;
}
- Die falsche Variante ruft
parseInitialOrdersbei jedem Render auf, obwohl React das Ergebnis nur beim ersten Mal benötigt. - Die richtige Variante übergibt eine Funktionsreferenz; React ruft sie nur beim ersten Mount auf.
5. useEffect – Grundlagen
useEffect führt einen Side Effect nach dem Rendern der Komponente aus (nachdem das DOM aktualisiert wurde).
import { useEffect, useState } from "react";
function PageTitleUpdater({ orderCount }) {
useEffect(() => {
document.title = `Du hast ${orderCount} Orders`;
});
return <p>Aktuelle Orders: {orderCount}</p>;
}
- Ohne Dependency Array (der zweite Parameter) läuft dieser Effect nach jedem Render — beim Mount und bei Updates.
document.titleist ein typischer Side Effect: Es interagiert mit der Außenwelt außerhalb von React (DOM API), nicht nur mit der Berechnung von Renderwerten.
6. useEffect: Dependency Array
Das Dependency Array bestimmt, wann der Effect läuft. Es gibt 3 Fälle, die du kennen solltest.
6.1. Kein Dependency Array
useEffect(() => {
console.log("Läuft nach JEDEM Render");
});
6.2. Leeres Dependency Array []
useEffect(() => {
console.log("Läuft NUR EINMAL nach dem Mount");
}, []);
6.3. Dependency Array mit Werten
function CustomerProfile({ customerId }) {
const [customer, setCustomer] = useState(null);
useEffect(() => {
fetchCustomer(customerId).then(setCustomer);
}, [customerId]);
return <p>{customer?.name ?? "Laden..."}</p>;
}
- Der Effect läuft nur neu, wenn sich
customerIdzwischen zwei Renders ändert (Vergleich mitObject.is). - Wenn du
customerIdnicht im Array aufführst, wird der Effect den altencustomerId-Wert verwenden (stale closure) — es wird der falsche Kunde abgerufen, wenn sich die Prop ändert.
Hinweis: Das ESLint-Plugin
eslint-plugin-react-hooksmit der Regelexhaustive-depswarnt dich, wenn du eine Dependency vergisst. Deaktiviere diese Regel nicht, es sei denn, du verstehst die Konsequenzen wirklich.
7. useEffect: Cleanup-Funktion
Wenn der Effect ein Subscription, Timer oder Event Listener erstellt, musst du eine Cleanup-Funktion zurückgeben, um ein Leak zu vermeiden.
function OrderStatusListener({ orderId }) {
const [status, setStatus] = useState("pending");
useEffect(() => {
const subscription = subscribeToOrderStatus(orderId, setStatus);
return () => {
subscription.unsubscribe();
};
}, [orderId]);
return <p>Status: {status}</p>;
}
- Die Cleanup-Funktion läuft bevor der Effect neu läuft (wenn sich
orderIdändert) und wenn die Komponente unmountet wird. - Ohne Cleanup wird jede
orderId-Änderung ein weiteres neues Subscription erstellen — am Ende wirdsetStatusvon vielen alten Subscriptions aufgerufen, was zu falschen Updates oder Memory Leaks führt.
Ausführungsreihenfolge, wenn orderId sich von 1 zu 2 ändert:
1. Cleanup des Effects mit orderId = 1
2. Neuer Effect mit orderId = 2
8. Häufige Fehler
8.1. Infinite Loop durch fehlendes korrektes Dependency
function BadExample() {
const [data, setData] = useState({ count: 0 });
useEffect(() => {
// neues Objekt bei jedem Render -> Effect sieht immer "Änderung", wenn data in deps aufgeführt ist
setData({ count: data.count + 1 }); // verursacht Re-Render -> Effect läuft wieder -> unendlich
}, [data]);
return <p>{data.count}</p>;
}
Behebe das mit functional updates und trenne Stoppbedingungen, oder entferne useEffect komplett, wenn diese Logik keine echten Side Effects benötigt.
8.2. Stale Closure in setTimeout/setInterval
function Countdown() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
const id = setInterval(() => {
setSeconds((prev) => prev - 1); // verwende functional updates, NICHT seconds direkt
}, 1000);
return () => clearInterval(id);
}, []); // leere deps, weil wir seconds nicht direkt im Effect lesen
return <p>{seconds}</p>;
}
- Wenn du stattdessen
setSeconds(seconds - 1)schreibst, wird die Closure immer dasseconds = 10aus der ersten Effect-Ausführung “merken” — der Countdown bleibt beim falschen Wert stecken.
8.3. useEffect zur Berechnung von abgeleiteten Werten verwenden
// FALSCH: redundant, verursacht zusätzliche Renders
function OrderSummaryBad({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
return <p>Gesamt: {total}</p>;
}
// RICHTIG: direkt während des Renders berechnen
function OrderSummaryGood({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Gesamt: {total}</p>;
}
Die “FALSCHE” Version verursacht zwei zusätzliche Renders: erst mit dem alten total, der Effect läuft, setTotal triggert einen zweiten Render mit dem korrekten Wert. Die “RICHTIGE” Version berechnet sofort während des Renders, braucht keinen Effect, keinen zusätzlichen State.
9. Wann man useEffect NICHT verwenden sollte
Hinweis:
useEffectist nur für die Synchronisierung mit Systemen außerhalb von React gedacht (DOM, APIs, Subscriptions, Timer). Wenn du einen Wert direkt aus vorhandenen Props/State berechnen kannst, verwende keinen Effect.
Drei Anzeichen, dass du es falsch machst:
- Du verwendest den Effect, um einen abgeleiteten Wert basierend auf anderem State zu berechnen (verwende stattdessen eine normale Variable oder
useMemo). - Du verwendest den Effect, um Benutzer-Events zu verarbeiten (verarbeite sie direkt im Handler
onClick,onSubmit). - Du verwendest den Effect, um State zurückzusetzen, wenn sich eine Prop ändert — oft kannst du stattdessen die
key-Prop verwenden, damit React die Komponente selbst neu mounted, statt einen Effect zu verwenden.
// Statt einen Effect zu verwenden, um das Formular zurückzusetzen, wenn sich customerId ändert:
// useEffect(() => { setForm(initialForm); }, [customerId]);
// Verwende die key-Prop, um die Komponente neu zu mounten, React setzt automatisch den ganzen internen State zurück:
<CustomerForm key={customerId} customerId={customerId} />
10. Hooks testen
Verwende Jest zusammen mit React Testing Library, um das tatsächliche Verhalten zu testen, statt Implementation Details.
npm install --save-dev @testing-library/react @testing-library/user-event jest
// CustomerCounter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomerCounter } from "./CustomerCounter";
test("erhöht count, wenn auf 'Kunde hinzufügen' geklickt wird", async () => {
const user = userEvent.setup();
render(<CustomerCounter />);
expect(screen.getByText("Kundenanzahl: 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Kunde hinzufügen" }));
expect(screen.getByText("Kundenanzahl: 1")).toBeInTheDocument();
});
- Der Test ruft
renderauf und simuliert dann echte Klicks überuserEvent, wie ein echter Benutzer. - Die Assertions basieren auf dem angezeigten Inhalt (
getByText), nicht auf internem State — das ist die richtige Art, React zu testen: Verhalte testen, nicht Implementation.
PASS ./CustomerCounter.test.jsx
✓ erhöht count, wenn auf 'Kunde hinzufügen' geklickt wird (45 ms)
11. Fazit
useState ist oberflächlich einfach, aber leicht fehlerhaft, wenn du vergisst, dass der Wert in der Closure pro Render “eingefroren” ist — verwende functional updates, wenn der neue State vom alten abhängt. useEffect ist mächtig, sollte aber nur zur Synchronisierung mit Systemen außerhalb von React verwendet werden; wenn ein Wert direkt während des Renders berechnet werden kann, wickle ihn nicht in einen Effect. Mit solidem Verständnis des Dependency Arrays und der Cleanup-Funktion vermeidest du die meisten Bugs, die mit diesen zwei Hooks verbunden sind.