Master the useEffect hook to handle side effects, API calls, and component lifecycle events in your React applications.
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.
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:
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?
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:
The second argument to useEffect is the dependency array. It controls when your effect runs:
useEffect(() => {
console.log('This runs after EVERY render');
// Rarely what you want!
});
useEffect(() => {
console.log('This runs ONCE when component mounts');
}, []); // Empty array = no dependencies
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>
);
}
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?
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.
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>
);
}
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>
);
}
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>;
}
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>;
}
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>
);
}
Let's put your useEffect knowledge to the test! Build a weather app with these features:
Requirements:
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>
);
}
// โ 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
}
// โ 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
}, []);
}
// โ 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
}
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
Test your useEffect knowledge:
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
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!