React Hooks: Der useEffect Hook

Basics-Trilogie

01.07.2025

dereact
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with: Alby

Inhaltsverzeichnis

    So, da sind wir wieder! Beim letzten Mal hab ich euch useRef gezeigt und dabei dieses mysteriöse useEffect in den Code geschmuggelt. Einige von euch haben sich vermutlich gefragt: "Was zur Hölle macht das denn?" Zeit, das Geheimnis zu lüften!

    Warum ich mit useRef angefangen hab und nicht mit den "Basics"? Keine Ahnung, aber jetzt macht's Sinn - ihr habt schon gesehen, wie die Hooks zusammenspielen. Das ist wie beim Kochen: Manchmal probiert man erst das fertige Gericht und lernt dann wie es zubereitet wird.

    Die Basics-Trilogie

    Dieser Artikel ist Teil zwei meiner etwas unkonventionellen "Basics-Trilogie":

    1. Der useRef Hook - Das Schweizer Taschenmesser
    2. Der useEffect Hook ← Du bist hier!
    3. Der useState Hook - Die "Basics" zum Schluss

    Für was ist der useEffect überhaupt?

    useEffect ist Reacts Art zu sagen: "Mach das, aber erst wenn ich mit dem ganzen Render-Kram fertig bin." Es ist der Hook für Seiteneffekte - also alles, was außerhalb von React passiert. API-Calls, Timer, DOM-Manipulation, Titel ändern, LocalStorage updaten... ihr versteht schon.

    Stellt euch vor, React ist wie ein Maler, der ein Bild malt (rendert). useEffect ist der Assistent, der wartet bis der Maler fertig ist und dann sagt: "So, jetzt häng ich das Bild auf und mach das Licht an."

    Der erste Kontakt - Ein simples Beispiel

    Fangen wir ganz harmlos an:

    1import { useState, useEffect } from "react"; 2 3function TitleChange() { 4 const [counter, setCounter] = useState<number>(0); 5 6 // useEffect waits until React has finished rendering 7 useEffect(() => { 8 console.log("Effect runs! Counter is:", counter); 9 document.title = `You clicked ${counter} times`; 10 }); 11 12 return ( 13 <div style={{ padding: "20px" }}> 14 <h2>Take a look at the browser tab! 👆</h2> 15 <p>Clicks: {counter}</p> 16 <button onClick={() => setCounter(counter + 1)}>Click me!</button> 17 </div> 18 ); 19} 20 21export default TitleChange;

    Was passiert hier? Nach JEDEM Render (also auch nach jedem Klick) läuft unser Effect und updated den Dokumenten-Titel. Cool, oder? Aber Moment... nach JEDEM Render? Das klingt nach Verschwendung...

    Das Dependency Array - Die Geheimwaffe

    Hier kommt der Game Changer: Das zweite Argument von useEffect. Es sagt React, wann der Effect laufen soll:

    1import { useState, useEffect } from "react"; 2 3function EffectControl() { 4 const [counter, setCounter] = useState<number>(0); 5 const [name, setName] = useState<string>(""); 6 7 // Effect runs only when 'counter' changes 8 useEffect(() => { 9 console.log("Counter Effect:", counter); 10 document.title = `Counter: ${counter}`; 11 }, [counter]); // 👈 Dependency Array! 12 13 // Effect runs only when 'name' changes 14 useEffect(() => { 15 console.log("Name Effect:", name); 16 if (name) { 17 localStorage.setItem("userName", name); 18 } 19 }, [name]); 20 21 // Effect runs only ONCE on mount 22 useEffect(() => { 23 console.log("🎉 Component mounted!"); 24 25 // Load saved name 26 const savedName: string | null = localStorage.getItem("userName"); 27 if (savedName) { 28 setName(savedName); 29 } 30 }, []); // 👈 Empty array = only on mount 31 32 return ( 33 <div style={{ padding: "20px" }}> 34 <h2>Effect Control Demo</h2> 35 36 <div style={{ marginBottom: "20px" }}> 37 <p>Counter: {counter}</p> 38 <button onClick={() => setCounter((c) => c + 1)}>+1</button> 39 </div> 40 41 <div> 42 <p>Name: {name || "No name yet"}</p> 43 <input 44 type="text" 45 value={name} 46 onChange={(e) => setName(e.target.value)} 47 placeholder="Your name..." 48 style={{ padding: "5px" }} 49 /> 50 </div> 51 52 <p style={{ marginTop: "20px", fontSize: "12px", color: "#666" }}> 53 Check the console for effect logs! 54 </p> 55 </div> 56 ); 57} 58 59export default EffectControl;

    Die Regeln sind simpel:

    • Kein Array: Effect läuft nach jedem Render
    • Leeres Array []: Effect läuft nur einmal beim Mount
    • Array mit Werten [a, b]: Effect läuft wenn sich a oder b ändert

    Cleanup - Aufräumen wie ein Profi

    Erinnert ihr euch an den Timer aus dem useRef Tutorial? Da war dieser mysteriöse return im Effect. Zeit zu erklären, was das macht:

    1import { useState, useEffect } from "react"; 2 3function ChatRoom() { 4 const [room, setRoom] = useState("general"); 5 const [messages, setMessages] = useState<string[]>([]); 6 7 useEffect(() => { 8 console.log(`➡️ Entering room: ${room}`); 9 10 // Simulate a chat connection 11 const connection = { 12 room, 13 active: true, 14 send: (msg: string) => console.log(`Send in ${room}:`, msg), 15 }; 16 17 // Simulate incoming messages 18 const interval = setInterval(() => { 19 if (connection.active) { 20 const newMessage = `Message in ${room} at ${new Date().toLocaleTimeString()}`; 21 setMessages((prev) => [...prev.slice(-4), newMessage]); 22 } 23 }, 3000); 24 25 // CLEANUP FUNCTION! Runs before the next effect or on unmount 26 return () => { 27 console.log(`⬅️ Leaving room: ${room}`); 28 connection.active = false; 29 clearInterval(interval); 30 }; 31 }, [room]); // Effect runs again when the room changes 32 33 return ( 34 <div style={{ padding: "20px" }}> 35 <h2>Chat Room Simulator</h2> 36 37 <div style={{ marginBottom: "20px" }}> 38 <button 39 onClick={() => setRoom("general")} 40 style={{ 41 marginRight: "10px", 42 fontWeight: room === "general" ? "bold" : "normal", 43 }} 44 > 45 #general 46 </button> 47 <button 48 onClick={() => setRoom("random")} 49 style={{ 50 marginRight: "10px", 51 fontWeight: room === "random" ? "bold" : "normal", 52 }} 53 > 54 #random 55 </button> 56 <button 57 onClick={() => setRoom("help")} 58 style={{ fontWeight: room === "help" ? "bold" : "normal" }} 59 > 60 #help 61 </button> 62 </div> 63 64 <div 65 style={{ 66 background: "#5d5555", 67 padding: "10px", 68 borderRadius: "5px", 69 minHeight: "150px", 70 }} 71 > 72 <h3>Messages in #{room}:</h3> 73 {messages.length === 0 ? ( 74 <p style={{ color: "#666" }}>Waiting for messages...</p> 75 ) : ( 76 messages.map((msg, i) => ( 77 <p key={i} style={{ margin: "5px 0" }}> 78 💬 {msg} 79 </p> 80 )) 81 )} 82 </div> 83 84 <p style={{ fontSize: "12px", color: "#666", marginTop: "10px" }}> 85 Check the console! You can see when rooms are entered/left. 86 </p> 87 </div> 88 ); 89} 90 91export default ChatRoom;

    Die Cleanup-Funktion im useEffect-Hook ist wichtig, um Memory Leaks zu verhindern, da sie sicherstellt, dass der Interval-Timer gestoppt und die Verbindung zurückgesetzt wird, bevor ein neuer Raum betreten wird oder die Komponente unmounted. Ohne dieses Aufräumen würden die Intervalle weiterlaufen und mehrfach erstellt werden, was zu Ressourcenverschwendung und unvorhersehbarem Verhalten führen kann.

    Die Cleanup-Function ist wie der Hausmeister - räumt auf, bevor der nächste reinkommt!

    Data Fetching - Der Klassiker

    Kein useEffect Tutorial ohne API-Calls! Aber Achtung, hier gibt's Fallen:

    1import { useState, useEffect } from "react"; 2 3interface GitHubUserData { 4 login: string; 5 name: string | null; 6 avatar_url: string; 7 location: string | null; 8 public_repos: number; 9 followers: number; 10 bio: string | null; 11} 12 13function GitHubUser() { 14 const [username, setUsername] = useState<string>("torvalds"); 15 const [userData, setUserData] = useState<GitHubUserData | null>(null); 16 const [loading, setLoading] = useState<boolean>(false); 17 const [error, setError] = useState<string | null>(null); 18 19 useEffect(() => { 20 // Important: We can't use an async function directly in useEffect! 21 const fetchUser = async () => { 22 setLoading(true); 23 setError(null); 24 25 try { 26 const response = await fetch( 27 `https://api.github.com/users/${username}` 28 ); 29 30 if (!response.ok) { 31 throw new Error("User not found!"); 32 } 33 34 const data = await response.json(); 35 setUserData(data); 36 } catch (err: unknown) { 37 const errorMessage = 38 err instanceof Error ? err.message : "Unknown error occurred"; 39 setError(errorMessage); 40 setUserData(null); 41 } finally { 42 setLoading(false); 43 } 44 }; 45 46 // Only fetch if username exists 47 if (username) { 48 fetchUser(); 49 } 50 51 // Cleanup is not needed here, but we could use an 52 // AbortController to cancel requests 53 }, [username]); // Fetch again when username changes 54 55 return ( 56 <div style={{ padding: "20px", maxWidth: "400px" }}> 57 <h2>GitHub User Finder</h2> 58 59 <input 60 type="text" 61 value={username} 62 onChange={(e) => setUsername(e.target.value)} 63 placeholder="GitHub Username..." 64 style={{ 65 width: "100%", 66 padding: "8px", 67 marginBottom: "20px", 68 fontSize: "16px", 69 }} 70 /> 71 72 {loading && <p>🔄 Loading...</p>} 73 74 {error && <p style={{ color: "red" }}>❌ Error: {error}</p>} 75 76 {userData && !loading && ( 77 <div 78 style={{ 79 background: "#5c4c4c", 80 padding: "20px", 81 borderRadius: "10px", 82 }} 83 > 84 <img 85 src={userData.avatar_url} 86 alt={userData.name || userData.login} 87 style={{ 88 width: "100px", 89 borderRadius: "50%", 90 marginBottom: "10px", 91 }} 92 /> 93 <h3>{userData.name || userData.login}</h3> 94 <p>📍 {userData.location || "Unknown"}</p> 95 <p>📦 Public Repos: {userData.public_repos}</p> 96 <p>👥 Followers: {userData.followers}</p> 97 {userData.bio && <p>💬 {userData.bio}</p>} 98 </div> 99 )} 100 </div> 101 ); 102} 103 104export default GitHubUser;

    Die häufigsten Fallen und wie ihr sie vermeidet

    Falle 1: Der Infinite Loop

    1// ❌ FALSCH - Infinite Loop! 2function InfiniteLoop() { 3 const [count, setCount] = useState(0); 4 5 useEffect(() => { 6 // This triggers a re-render, which triggers the effect, which... 7 setCount(count + 1); 8 }); // No dependency array = runs after EVERY render! 9 10 return <div>I’m going to crash your browser: {count}</div>; 11} 12 13// ✅ CORRECT - Controlled Effect 14function ControlledCounter() { 15 const [count, setCount] = useState(0); 16 17 useEffect(() => { 18 // Runs only once on mount 19 setCount(1); 20 }, []); // Empty array! 21 22 return <div>Safe counter: {count}</div>; 23}

    Was passiert hier? Der Effect ändert den State, was ein Re-Render auslöst, was den Effect wieder ausführt, was den State ändert... und dein Browser sagt irgendwann "Mir reicht's!" Das ist wie eine Katze, die ihrem eigenen Schwanz hinterherläuft - nur dass hier deine CPU schmilzt.

    Falle 2: Vergessene Dependencies

    1// ❌ WRONG - ESLint will complain! 2function ForgetfulComponent({ userId }) { 3 const [user, setUser] = useState(null); 4 5 useEffect(() => { 6 fetchUser(userId).then(setUser); 7 }, []); // userId is missing in the array! 8 9 return <div>{user?.name}</div>; 10} 11 12// ✅ CORRECT - All dependencies included 13function AttentiveComponent({ userId }) { 14 const [user, setUser] = useState(null); 15 16 useEffect(() => { 17 fetchUser(userId).then(setUser); 18 }, [userId]); // userId is included! 19 20 return <div>{user?.name}</div>; 21}

    Was ist das Problem? Der Effect läuft nur einmal beim Mount, aber wenn sich die userId ändert, bleibt der alte User stehen. Hier habe ich gerade keine passende Analogie für übrig. ESLint ist hier dein bester Freund - hör auf die Warnungen!

    Falle 3: Race Conditions

    1function RaceConditionDemo() { 2 const [query, setQuery] = useState(''); 3 const [results, setResults] = useState([]); 4 5 useEffect(() => { 6 let cancelled = false; // 👈 The trick! 7 8 async function search() { 9 const data = await searchAPI(query); 10 11 // Only set results if this effect is still "active" 12 if (!cancelled) { 13 setResults(data); 14 } 15 } 16 17 if (query) { 18 search(); 19 } 20 21 // Cleanup: mark this effect as "cancelled" 22 return () => { 23 cancelled = true; 24 }; 25 }, [query]); 26 27 return ( 28 <div> 29 <input 30 value={query} 31 onChange={(e) => setQuery(e.target.value)} 32 placeholder="Search..." 33 /> 34 {results.map(r => <div key={r.id}>{r.title}</div>)} 35 </div> 36 ); 37}

    Warum brauchen wir das? Stell dir vor, du tippst "React" ein, aber die Suche für "R" dauert länger als für "React". Ohne das cancelled Flag könnten die Ergebnisse für "R" die für "React" überschreiben - obwohl sie später ankommen! Das ist wie wenn du Pizza und Burger bestellst, aber die Pizza-Antwort den Burger überschreibt, nur weil der Pizzabote im Stau stand.

    Custom Hooks mit useEffect

    Lasst uns das Gelernte in einen wiederverwendbaren Hook packen:

    1import { useState, useEffect } from "react"; 2 3interface WindowSize { 4 width: number; 5 height: number; 6} 7 8// Our custom hook for window size 9function useWindowSize() { 10 const [windowSize, setWindowSize] = useState<WindowSize>({ 11 width: window.innerWidth, 12 height: window.innerHeight, 13 }); 14 15 useEffect(() => { 16 const handleResize = (): void => { 17 setWindowSize({ 18 width: window.innerWidth, 19 height: window.innerHeight, 20 }); 21 }; 22 23 // Add event listener 24 window.addEventListener("resize", handleResize); 25 26 // Cleanup: remove event listener 27 return () => { 28 window.removeEventListener("resize", handleResize); 29 }; 30 }, []); // Empty = only on mount/unmount 31 32 return windowSize; 33} 34 35// Component that uses our hook 36function ResponsiveDemo() { 37 const { width, height } = useWindowSize(); 38 39 const getDevice = (): string => { 40 if (width < 640) return "📱 Mobile"; 41 if (width < 1024) return "📱 Tablet"; 42 return "💻 Desktop"; 43 }; 44 45 return ( 46 <div 47 style={{ 48 padding: "20px", 49 background: 50 width < 640 ? "#ffe0e0" : width < 1024 ? "#e0f0ff" : "#e0ffe0", 51 minHeight: "200px", 52 transition: "background 0.3s", 53 }} 54 > 55 <h2>Responsive Hook Demo</h2> 56 <p>Window width: {width}px</p> 57 <p>Window height: {height}px</p> 58 <p>Device: {getDevice()}</p> 59 <p style={{ fontSize: "12px", color: "#666" }}> 60 Try resizing the window! 61 </p> 62 </div> 63 ); 64} 65 66export default ResponsiveDemo;

    useEffect ist ein unverzichtbarer Hook für die Verwaltung von Seiteneffekten in React, wobei das korrekte Dependency-Array und die Implementierung von Cleanup-Funktionen entscheidend sind, um Memory Leaks zu vermeiden und die Performance zu optimieren. Wie in diesem Beispiel zu sehen, ermöglicht die Cleanup-Funktion das sichere Entfernen von Event-Listenern, wenn Komponenten unmounten oder Dependencies sich ändern.

    Die goldenen useEffect Regeln

    Nach all den Beispielen, hier meine persönlichen Regeln für useEffect:

    1. Effects sind für Seiteneffekte - Nicht für Berechnungen (dafür gibt's useMemo)
    2. Cleanup ist dein Freund - Räum immer auf (Timer, Listener, Subscriptions)
    3. Dependencies nicht vergessen - ESLint hilft dir dabei
    4. Kein async direkt in useEffect - Erstelle eine async Funktion innerhalb
    5. Race Conditions bedenken - Cancelled-Flag oder AbortController nutzen
    6. Weniger ist mehr - Ein Effect pro Aufgabe, nicht alles in einen packen

    Wann brauche ich useEffect?

    useEffect ist richtig für:

    • API Calls / Data Fetching
    • Timer und Intervalle
    • Event Listener (window, document)
    • LocalStorage/SessionStorage
    • Third-Party Libraries initialisieren
    • DOM-Manipulation nach dem Render
    • Subscriptions (WebSocket, EventEmitter)

    useEffect ist NICHT für:

    • Berechnungen basierend auf Props/State → useMemo
    • Event Handler → normale Funktionen
    • Initialer State → useState mit Funktion

    Zum Abschluss

    useEffect ist wie der Kleber zwischen React und der Außenwelt. Es lässt deine Komponenten mit APIs sprechen, auf Browser-Events reagieren und Dinge tun, die über das reine Rendern hinausgehen.

    Am Anfang kann es verwirrend sein - "Wann läuft was?" und "Warum läuft mein Effect zweimal?" (Spoiler: StrictMode in Development). Aber mit ein bisschen Übung wird es zur zweiten Natur.

    Und denkt dran: Ihr habt useEffect schon im Timer-Beispiel beim letzten Mal gesehen. Ihr wusstet nur noch nicht, wie mächtig dieser kleine Hook ist!

    Nächstes Mal? Vielleicht useState - ja, ich mach die Basics zum Schluss. Oder useContext für globalen State. Oder useReducer für die Redux-Nostalgiker unter euch. Mal schauen!

    Bis dahin: Happy Coding und vergesst das Cleanup nicht! 🧹

    Die Trilogie geht weiter!

    Das war Teil 2 der Basics-Trilogie. Ihr wisst jetzt, wie useRef und useEffect zusammenarbeiten. Bereit für das große Finale?

    Weiter geht's mit Der useState Hook

    Ja, ich erkläre euch useState zum Schluss. Warum? Weil ihr jetzt versteht, warum State Re-Renders triggert und wie das mit Effects zusammenhängt. Trust the process!

    Verpasst? Der useRef Hook erklärt das Schweizer Taschenmesser unter den Hooks.

    Alle Beispiele für CodeSandbox

    1. TitelÄnderer - Der erste Kontakt mit useEffect
    2. EffectKontrolle - Dependency Arrays verstehen
    3. ChatRaum - Cleanup Functions meistern
    4. GitHubUser - API Calls richtig machen
    5. ResponsiveDemo - Custom Hook mit Window-Events

    Weiterführende Links

    Comments: