useEffect Hook
Master the useEffect hook to handle side effects, API calls, and component lifecycle events in your React applications.
Learning Objectives
- Understand what side effects are and why they matter
- Learn to use useEffect for component lifecycle events
- Master API calls and data fetching with useEffect
- Handle cleanup to prevent memory leaks
- Control when effects run with dependencies
- Build real-world applications with useEffect
useEffect Hook
Welcome to one of the most powerful and frequently used hooks in React! If useState manages what your component remembers, then useEffect manages what your component does.
Think of useEffect as your component's way of saying: "Hey, I need to do something after I appear on screen" or "I need to clean up before I disappear." It's like having a personal assistant for your component that handles all the behind-the-scenes work.
What Are Side Effects?
Before diving into useEffect, let's understand side effects. In programming, a side effect is anything that affects something outside the current function. In React components, side effects include:
🌐 Fetching data from APIs
⏰ Setting up timers or intervals
🎧 Adding event listeners
📊 Updating the document title
💾 Saving to localStorage
🔌 Connecting to websockets
Here's the problem: You can't do side effects directly in your component body!
function BadExample() { const [data, setData] = useState(null); // ❌ This will cause problems! fetch('/api/data') .then(response => response.json()) .then(data => setData(data)); return <div>{data}</div>; }
Why is this bad? This code runs every time the component renders, which means:
- Infinite API calls! 🔄
- Poor performance 🐌
- Potential crashes 💥
useEffect to the Rescue!
useEffect lets you perform side effects safely. Here's the basic syntax:
import React, { useState, useEffect } from 'react'; function MyComponent() { const [data, setData] = useState(null); useEffect(() => { // Side effect code goes here fetch('/api/data') .then(response => response.json()) .then(data => setData(data)); }, []); // Dependencies array (we'll learn about this!) return <div>{data ? data.message : 'Loading...'}</div>; }
What's happening here?
- useEffect takes a function that contains your side effect
- The function runs after the component renders
- The empty array "[]" means "only run once when component mounts"
Your First useEffect Example
Let's build a simple component that shows the current time and updates every second:
function LiveClock() { const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString()); useEffect(() => { console.log('Setting up timer...'); const timer = setInterval(() => { setCurrentTime(new Date().toLocaleTimeString()); }, 1000); // Cleanup function (important!) return () => { console.log('Cleaning up timer...'); clearInterval(timer); }; }, []); // Empty dependency array = run once on mount return ( <div style={{ fontSize: '24px', fontFamily: 'monospace', textAlign: 'center', padding: '20px' }}> 🕐 Current Time: {currentTime} </div> ); }
Key concepts in this example:
- ✅ Setup: Create a timer when component appears
- ✅ Update: Timer updates state every second
- ✅ Cleanup: Clear timer when component disappears
Understanding the Dependency Array
The second argument to useEffect is the dependency array. It controls when your effect runs:
No Dependency Array: Runs After Every Render
useEffect(() => { console.log('This runs after EVERY render'); // Rarely what you want! });
Empty Array: Runs Once on Mount
useEffect(() => { console.log('This runs ONCE when component mounts'); }, []); // Empty array = no dependencies
With Dependencies: Runs When Dependencies Change
useEffect(() => { console.log('This runs when count changes'); }, [count]); // Runs when 'count' changes
Let's see this in action:
function EffectDemo() { const [count, setCount] = useState(0); const [name, setName] = useState(''); // Runs after every render useEffect(() => { console.log('After every render'); }); // Runs once on mount useEffect(() => { console.log('Component mounted!'); }, []); // Runs when count changes useEffect(() => { console.log('Count changed to: ' + count); document.title = 'Count: ' + count; }, [count]); // Runs when name changes useEffect(() => { console.log('Name changed to: ' + name); }, [name]); return ( <div style={{ padding: '20px' }}> <h2>useEffect Demo</h2> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment Count </button> <br /><br /> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter your name" /> <p>Name: {name}</p> <p><em>Open the console to see useEffect in action!</em></p> </div> ); }
Real-World Example: Fetching User Data
Let's build a user profile component that fetches data from an API:
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // Reset state when userId changes setLoading(true); setError(null); setUser(null); // Simulate API call const fetchUser = async () => { try { console.log('Fetching user ' + userId + '...'); // Simulate network delay await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate different responses if (userId === 999) { throw new Error('User not found'); } // Mock user data const userData = { id: userId, name: 'User ' + userId, email: 'user' + userId + '@example.com', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + userId, joinDate: '2023-01-15', posts: Math.floor(Math.random() * 100), followers: Math.floor(Math.random() * 1000) }; setUser(userData); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUser(); }, [userId]); // Re-run when userId changes if (loading) { return ( <div style={{ padding: '20px', textAlign: 'center' }}> <div>🔄 Loading user data...</div> </div> ); } if (error) { return ( <div style={{ padding: '20px', textAlign: 'center', backgroundColor: '#ffebee', border: '1px solid #e57373', borderRadius: '5px' }}> <div>❌ Error: {error}</div> </div> ); } return ( <div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '10px', maxWidth: '400px', margin: '20px auto' }}> <div style={{ textAlign: 'center', marginBottom: '15px' }}> <img src={user.avatar} alt={user.name} style={{ width: '80px', height: '80px', borderRadius: '50%', border: '3px solid #4CAF50' }} /> </div> <h2 style={{ textAlign: 'center', margin: '10px 0' }}> {user.name} </h2> <div style={{ color: '#666', textAlign: 'center' }}> <p>📧 {user.email}</p> <p>📅 Joined: {user.joinDate}</p> <p>📝 Posts: {user.posts}</p> <p>👥 Followers: {user.followers}</p> </div> </div> ); } // Demo component to test UserProfile function UserProfileDemo() { const [selectedUserId, setSelectedUserId] = useState(1); return ( <div> <h2>User Profile Demo</h2> <div style={{ marginBottom: '20px' }}> <label>Select User ID: </label> <select value={selectedUserId} onChange={(e) => setSelectedUserId(Number(e.target.value))} > <option value={1}>User 1</option> <option value={2}>User 2</option> <option value={3}>User 3</option> <option value={999}>User 999 (Error Demo)</option> </select> </div> <UserProfile userId={selectedUserId} /> </div> ); }
What makes this example great?
- ✅ Handles loading, success, and error states
- ✅ Re-fetches data when userId changes
- ✅ Resets state appropriately
- ✅ Uses async/await for clean code
Cleanup: Preventing Memory Leaks
One of the most important aspects of useEffect is cleanup. When you set up subscriptions, timers, or event listeners, you must clean them up to prevent memory leaks.
Timer Cleanup Example
function CountdownTimer({ startFrom = 10 }) { const [timeLeft, setTimeLeft] = useState(startFrom); const [isActive, setIsActive] = useState(false); useEffect(() => { let interval = null; if (isActive && timeLeft > 0) { interval = setInterval(() => { setTimeLeft(timeLeft => timeLeft - 1); }, 1000); } // Cleanup function return () => { if (interval) { clearInterval(interval); } }; }, [isActive, timeLeft]); const resetTimer = () => { setTimeLeft(startFrom); setIsActive(false); }; return ( <div style={{ textAlign: 'center', padding: '20px' }}> <h2>⏰ Countdown Timer</h2> <div style={{ fontSize: '48px', margin: '20px 0' }}> {timeLeft} </div> {timeLeft === 0 ? ( <div> <p style={{ fontSize: '24px', color: 'red' }}>🎉 Time's up!</p> <button onClick={resetTimer}>Reset</button> </div> ) : ( <div> <button onClick={() => setIsActive(!isActive)}> {isActive ? 'Pause' : 'Start'} </button> <button onClick={resetTimer} style={{ marginLeft: '10px' }}> Reset </button> </div> )} </div> ); }
Event Listener Cleanup Example
function WindowSizeTracker() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); } // Add event listener window.addEventListener('resize', handleResize); // Cleanup function return () => { window.removeEventListener('resize', handleResize); }; }, []); // Empty dependency array = setup once, cleanup on unmount return ( <div style={{ padding: '20px' }}> <h2>📏 Window Size Tracker</h2> <p>Width: {windowSize.width}px</p> <p>Height: {windowSize.height}px</p> <p><em>Try resizing your browser window!</em></p> </div> ); }
Common useEffect Patterns
Pattern 1: Fetch Data on Mount
function DataComponent() { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { const response = await fetch('/api/data'); const result = await response.json(); setData(result); } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; fetchData(); }, []); // Run once on mount return <div>{/* render data */}</div>; }
Pattern 2: Update Document Title
function PageComponent({ pageTitle }) { useEffect(() => { const previousTitle = document.title; document.title = pageTitle; // Restore previous title on cleanup return () => { document.title = previousTitle; }; }, [pageTitle]); return <div>Page content</div>; }
Pattern 3: Local Storage Sync
function useLocalStorage(key, initialValue) { const [value, setValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { return initialValue; } }); useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error('Error saving to localStorage:', error); } }, [key, value]); return [value, setValue]; } // Usage function SettingsComponent() { const [theme, setTheme] = useLocalStorage('theme', 'light'); return ( <div> <p>Current theme: {theme}</p> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> </div> ); }
Practice Exercise: Build a Weather App
Let's put your useEffect knowledge to the test! Build a weather app with these features:
Requirements:
- Fetch weather data when component mounts
- Update every 30 seconds
- Handle loading and error states
- Clean up timer on unmount
- Allow manual refresh
Starter code:
function WeatherApp() { const [weather, setWeather] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const fetchWeather = async () => { try { setLoading(true); setError(null); // Simulate API call await new Promise(resolve => setTimeout(resolve, 1000)); // Mock weather data const weatherData = { city: 'New York', temperature: Math.round(Math.random() * 30 + 50), // 50-80°F condition: ['sunny', 'cloudy', 'rainy'][Math.floor(Math.random() * 3)], humidity: Math.round(Math.random() * 50 + 30), // 30-80% windSpeed: Math.round(Math.random() * 20 + 5) // 5-25 mph }; setWeather(weatherData); setLastUpdated(new Date()); } catch (err) { setError('Failed to fetch weather data'); } finally { setLoading(false); } }; // Your useEffect code here! // 1. Fetch weather on mount // 2. Set up 30-second auto-refresh // 3. Clean up timer const getWeatherIcon = (condition) => { switch(condition) { case 'sunny': return '☀️'; case 'cloudy': return '☁️'; case 'rainy': return '🌧️'; default: return '🌤️'; } }; return ( <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}> <h2>🌤️ Weather App</h2> {/* Your UI code here */} </div> ); }
Common useEffect Mistakes
Mistake 1: Missing Dependencies
// ❌ Missing 'count' in dependencies function BadExample() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // This will always log 0! }, 1000); return () => clearInterval(timer); }, []); // Missing 'count' dependency } // ✅ Include all dependencies function GoodExample() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // This logs the current count }, 1000); return () => clearInterval(timer); }, [count]); // Include 'count' dependency }
Mistake 2: Not Cleaning Up
// ❌ Memory leak - timer never cleaned up function BadTimer() { useEffect(() => { setInterval(() => { console.log('Timer tick'); }, 1000); // No cleanup! }, []); } // ✅ Proper cleanup function GoodTimer() { useEffect(() => { const timer = setInterval(() => { console.log('Timer tick'); }, 1000); return () => clearInterval(timer); // Cleanup }, []); }
Mistake 3: Infinite Loops
// ❌ Infinite loop - creates new object every render function BadExample() { const [user, setUser] = useState(null); useEffect(() => { fetchUser().then(setUser); }, [{ id: 1 }]); // New object every time! } // ✅ Use stable references function GoodExample() { const [user, setUser] = useState(null); const userId = 1; // Stable value useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); // Stable dependency }
What We've Learned
Congratulations! You now understand:
✅ What side effects are and why useEffect is needed
✅ How to perform effects after component renders
✅ How to control when effects run with dependencies
✅ How to clean up effects to prevent memory leaks
✅ Common patterns for data fetching and timers
✅ How to avoid common useEffect pitfalls
Quick Recap Quiz
Test your useEffect knowledge:
- When does a useEffect with an empty dependency array run?
- What happens if you don't include a dependency that your effect uses?
- Why is cleanup important in useEffect?
- How do you prevent a useEffect from running on every render?
Answers: 1) Once when component mounts, 2) You get stale closure bugs, 3) Prevents memory leaks and unwanted behavior, 4) Use dependency array to control when it runs
What's Next?
In our next lesson, we'll learn about useContext - React's solution for sharing data between components without prop drilling. You'll discover how to create global state that any component can access, making your apps more organized and easier to manage.
useEffect handles what your components do, and useContext handles what your components share!