Master the useEffect hook for side effects, data fetching, subscriptions, and cleanup in functional components. Learn lifecycle patterns and async operations.
The useEffect hook is React's Swiss Army knife for side effects. While useState manages what your component remembers, useEffect manages what your component does - like fetching data, updating the DOM, setting up subscriptions, or cleaning up resources.
Think of useEffect as combining three class component lifecycle methods into one powerful hook: componentDidMount, componentDidUpdate, and componentWillUnmount. This consolidation makes functional components incredibly powerful and easier to reason about.
In this lesson, you'll master useEffect patterns that are essential for building real-world React applications - from simple DOM updates to complex data fetching and resource management.
Side effects are operations that affect something outside the scope of the current function. In React components, side effects include data fetching, subscriptions, timers, and DOM manipulation.
What are Side Effects?
Side effects are anything that affects the "outside world":
Why useEffect?
React components should be pure functions for their rendering logic, but real applications need side effects. useEffect provides a clean way to perform side effects without breaking React's rendering model.
Basic useEffect Syntax:
1import React, { useState, useEffect } from 'react';
2
3function MyComponent() {
4 const [count, setCount] = useState(0);
5
6 // Effect runs after every render
7 useEffect(() => {
8 document.title = `Count: ${count}`;
9 });
10
11 return (
12 <div>
13 <p>Count: {count}</p>
14 <button onClick={() => setCount(count + 1)}>
15 Increment
16 </button>
17 </div>
18 );
19}When useEffect Runs:
The dependency array is useEffect's second argument that controls when the effect should re-run.
Three Dependency Patterns:
1. No Dependencies - Runs Every Render:
1useEffect(() => {
2 console.log('Runs after every render');
3});2. Empty Dependencies - Runs Once (Mount Only):
1useEffect(() => {
2 console.log('Runs only once after mount');
3}, []); // Empty array = no dependencies3. Specific Dependencies - Runs When Dependencies Change:
1function UserProfile({ userId }) {
2 const [user, setUser] = useState(null);
3 const [posts, setPosts] = useState([]);
4
5 // Runs when userId changes
6 useEffect(() => {
7 fetchUser(userId).then(setUser);
8 }, [userId]);
9
10 // Runs when user changes
11 useEffect(() => {
12 if (user) {
13 fetchUserPosts(user.id).then(setPosts);
14 }
15 }, [user]);
16
17 return (
18 <div>
19 {user && <h1>{user.name}</h1>}
20 <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>
21 </div>
22 );
23}Dependency Rules:
react-hooks/exhaustive-deps to catch missing dependenciesCommon Dependency Mistakes:
1// ❌ Missing dependency
2function BadExample({ userId }) {
3 const [user, setUser] = useState(null);
4
5 useEffect(() => {
6 fetchUser(userId).then(setUser); // userId not in deps!
7 }, []); // This will only fetch once, ignoring userId changes
8}
9
10// ✅ Correct dependencies
11function GoodExample({ userId }) {
12 const [user, setUser] = useState(null);
13
14 useEffect(() => {
15 fetchUser(userId).then(setUser);
16 }, [userId]); // Now it refetches when userId changes
17}useEffect is perfect for data fetching, but async operations require special handling.
Basic Data Fetching Pattern:
1function UserList() {
2 const [users, setUsers] = useState([]);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 // Create async function inside effect
8 async function fetchUsers() {
9 try {
10 setLoading(true);
11 const response = await fetch('/api/users');
12
13 if (!response.ok) {
14 throw new Error('Failed to fetch users');
15 }
16
17 const userData = await response.json();
18 setUsers(userData);
19 } catch (err) {
20 setError(err.message);
21 } finally {
22 setLoading(false);
23 }
24 }
25
26 fetchUsers();
27 }, []); // Fetch once on mount
28
29 if (loading) return <div>Loading users...</div>;
30 if (error) return <div>Error: {error}</div>;
31
32 return (
33 <ul>
34 {users.map(user => (
35 <li key={user.id}>{user.name}</li>
36 ))}
37 </ul>
38 );
39}Advanced Data Fetching with Dependencies:
1function SearchResults({ query, filters }) {
2 const [results, setResults] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 // Don't fetch if no query
7 if (!query.trim()) {
8 setResults([]);
9 return;
10 }
11
12 async function searchData() {
13 setLoading(true);
14 try {
15 const response = await fetch(`/api/search?${new URLSearchParams({
16 q: query,
17 ...filters
18 })}`);
19
20 const data = await response.json();
21 setResults(data.results);
22 } catch (error) {
23 console.error('Search failed:', error);
24 setResults([]);
25 } finally {
26 setLoading(false);
27 }
28 }
29
30 // Debounce search requests
31 const timeoutId = setTimeout(searchData, 300);
32
33 // Cleanup timeout if effect runs again
34 return () => clearTimeout(timeoutId);
35 }, [query, filters]); // Refetch when query or filters change
36
37 return (
38 <div>
39 {loading && <div>Searching...</div>}
40 <div>Found {results.length} results</div>
41 {results.map(item => (
42 <div key={item.id}>{item.title}</div>
43 ))}
44 </div>
45 );
46}Race Condition Prevention:
1useEffect(() => {
2 let cancelled = false;
3
4 async function fetchData() {
5 const response = await fetch(`/api/data/${id}`);
6 const data = await response.json();
7
8 // Only update state if effect hasn't been cancelled
9 if (!cancelled) {
10 setData(data);
11 }
12 }
13
14 fetchData();
15
16 // Cleanup function runs when effect is cleaned up
17 return () => {
18 cancelled = true;
19 };
20}, [id]);Cleanup functions prevent memory leaks and cancel ongoing operations when components unmount or effects change.
Timer Cleanup:
1function Timer() {
2 const [seconds, setSeconds] = useState(0);
3
4 useEffect(() => {
5 const intervalId = setInterval(() => {
6 setSeconds(prev => prev + 1);
7 }, 1000);
8
9 // Cleanup function - runs when:
10 // 1. Component unmounts
11 // 2. Dependencies change (effect re-runs)
12 return () => {
13 clearInterval(intervalId);
14 };
15 }, []); // Empty deps = cleanup only on unmount
16
17 return <div>Timer: {seconds} seconds</div>;
18}Event Listener Cleanup:
1function WindowSize() {
2 const [size, setSize] = useState({ width: 0, height: 0 });
3
4 useEffect(() => {
5 function updateSize() {
6 setSize({
7 width: window.innerWidth,
8 height: window.innerHeight,
9 });
10 }
11
12 // Set initial size
13 updateSize();
14
15 // Add event listener
16 window.addEventListener('resize', updateSize);
17
18 // Cleanup - remove event listener
19 return () => {
20 window.removeEventListener('resize', updateSize);
21 };
22 }, []); // No dependencies needed
23
24 return <div>Window size: {size.width} x {size.height}</div>;
25}Subscription Cleanup:
1function ChatRoom({ roomId }) {
2 const [messages, setMessages] = useState([]);
3
4 useEffect(() => {
5 const chatConnection = connectToChat(roomId);
6
7 chatConnection.on('message', (message) => {
8 setMessages(prev => [...prev, message]);
9 });
10
11 chatConnection.on('error', (error) => {
12 console.error('Chat error:', error);
13 });
14
15 // Cleanup subscription
16 return () => {
17 chatConnection.disconnect();
18 };
19 }, [roomId]); // Reconnect when room changes
20
21 return (
22 <div>
23 <h3>Room: {roomId}</h3>
24 {messages.map(msg => (
25 <div key={msg.id}>{msg.text}</div>
26 ))}
27 </div>
28 );
29}AbortController for Fetch Cleanup:
1useEffect(() => {
2 const controller = new AbortController();
3
4 async function fetchData() {
5 try {
6 const response = await fetch('/api/data', {
7 signal: controller.signal
8 });
9 const data = await response.json();
10 setData(data);
11 } catch (error) {
12 if (error.name !== 'AbortError') {
13 console.error('Fetch error:', error);
14 }
15 }
16 }
17
18 fetchData();
19
20 // Cancel fetch on cleanup
21 return () => {
22 controller.abort();
23 };
24}, []);Build a weather dashboard that demonstrates data fetching, cleanup, multiple effects, and real-time updates using useEffect patterns.
1// Weather Dashboard Component
2function WeatherDashboard() {
3 const [location, setLocation] = useState('London');
4 const [weather, setWeather] = useState(null);
5 const [forecast, setForecast] = useState([]);
6 const [loading, setLoading] = useState(false);
7 const [error, setError] = useState(null);
8 const [lastUpdated, setLastUpdated] = useState(null);
9
10 // Effect 1: Fetch current weather when location changes
11 useEffect(() => {
12 async function fetchWeather() {
13 if (!location) return;
14
15 setLoading(true);
16 setError(null);
17
18 try {
19 const response = await fetch(`/api/weather/current?${new URLSearchParams({
20 location: location
21 })}`);
22
23 if (!response.ok) {
24 throw new Error('Weather data not available');
25 }
26
27 const data = await response.json();
28 setWeather(data);
29 setLastUpdated(new Date());
30 } catch (err) {
31 setError(err.message);
32 setWeather(null);
33 } finally {
34 setLoading(false);
35 }
36 }
37
38 fetchWeather();
39 }, [location]); // Refetch when location changes
40
41 // Effect 2: Fetch forecast data when weather is loaded
42 useEffect(() => {
43 if (!weather || !weather.coordinates) return;
44
45 async function fetchForecast() {
46 try {
47 const response = await fetch(`/api/weather/forecast?${new URLSearchParams({
48 lat: weather.coordinates.lat,
49 lon: weather.coordinates.lon
50 })}`);
51
52 const data = await response.json();
53 setForecast(data.forecast);
54 } catch (err) {
55 console.error('Failed to fetch forecast:', err);
56 }
57 }
58
59 fetchForecast();
60 }, [weather]); // Fetch forecast when weather data changes
61
62 // Effect 3: Auto-refresh weather every 5 minutes
63 useEffect(() => {
64 if (!weather) return; // Don't start timer if no weather data
65
66 const intervalId = setInterval(() => {
67 // Trigger refresh by updating a dependency
68 setLocation(prev => prev); // This will trigger the first effect
69 }, 5 * 60 * 1000); // 5 minutes
70
71 // Cleanup timer
72 return () => {
73 clearInterval(intervalId);
74 };
75 }, [weather]); // Restart timer when weather changes
76
77 // Effect 4: Document title updates
78 useEffect(() => {
79 if (weather) {
80 document.title = `${weather.temperature}°C in ${location} - Weather App`;
81 } else {
82 document.title = 'Weather App';
83 }
84
85 // Cleanup - restore original title when component unmounts
86 return () => {
87 document.title = 'React Learning Platform';
88 };
89 }, [weather, location]);
90
91 // Effect 5: Keyboard shortcuts
92 useEffect(() => {
93 function handleKeyPress(event) {
94 if (event.ctrlKey && event.key === 'r') {
95 event.preventDefault();
96 setLocation(prev => prev); // Trigger refresh
97 }
98 }
99
100 document.addEventListener('keydown', handleKeyPress);
101
102 // Cleanup event listener
103 return () => {
104 document.removeEventListener('keydown', handleKeyPress);
105 };
106 }, []); // No dependencies needed for global keyboard listener
107
108 const handleLocationChange = (newLocation) => {
109 setLocation(newLocation);
110 };
111
112 return (
113 <div className="weather-dashboard">
114 <header className="dashboard-header">
115 <h1>🌤️ Weather Dashboard</h1>
116 <div className="location-controls">
117 <input
118 type="text"
119 value={location}
120 onChange={(e) => handleLocationChange(e.target.value)}
121 placeholder="Enter city name..."
122 className="location-input"
123 />
124 <button
125 onClick={() => handleLocationChange(location)}
126 disabled={loading}
127 className="refresh-btn"
128 >
129 {loading ? '🔄 Loading...' : '🔄 Refresh'}
130 </button>
131 </div>
132 </header>
133
134 {error && (
135 <div className="error-message">
136 ❌ Error: {error}
137 </div>
138 )}
139
140 {loading && (
141 <div className="loading-spinner">
142 🌀 Loading weather data...
143 </div>
144 )}
145
146 {weather && (
147 <div className="weather-current">
148 <div className="weather-main">
149 <h2>{weather.location}</h2>
150 <div className="temperature">
151 {weather.temperature}°C
152 </div>
153 <div className="description">
154 {weather.description}
155 </div>
156 <div className="details">
157 <span>💨 Wind: {weather.windSpeed} km/h</span>
158 <span>💧 Humidity: {weather.humidity}%</span>
159 <span>👁️ Visibility: {weather.visibility} km</span>
160 </div>
161 </div>
162 </div>
163 )}
164
165 {forecast.length > 0 && (
166 <div className="forecast-section">
167 <h3>7-Day Forecast</h3>
168 <div className="forecast-grid">
169 {forecast.map((day, index) => (
170 <div key={index} className="forecast-item">
171 <div className="forecast-day">{day.day}</div>
172 <div className="forecast-temp">
173 {day.high}°/{day.low}°
174 </div>
175 <div className="forecast-desc">{day.description}</div>
176 </div>
177 ))}
178 </div>
179 </div>
180 )}
181
182 {lastUpdated && (
183 <div className="last-updated">
184 ⏰ Last updated: {lastUpdated.toLocaleTimeString()}
185 </div>
186 )}
187
188 <div className="tips">
189 <h4>💡 useEffect Patterns Demonstrated:</h4>
190 <ul>
191 <li>✅ Data fetching with dependencies</li>
192 <li>✅ Cleanup functions for timers</li>
193 <li>✅ Event listener management</li>
194 <li>✅ Multiple coordinated effects</li>
195 <li>✅ Conditional effect execution</li>
196 <li>✅ Document title updates</li>
197 </ul>
198 <p><kbd>Ctrl+R</kbd> to refresh weather data</p>
199 </div>
200 </div>
201 );
202}
203
204// Mock API functions (replace with real API calls)
205async function fetchCurrentWeather(location) {
206 // Simulate API delay
207 await new Promise(resolve => setTimeout(resolve, 1000));
208
209 return {
210 location: location,
211 temperature: Math.round(Math.random() * 30) + 5,
212 description: ['Sunny', 'Cloudy', 'Partly Cloudy', 'Rainy'][Math.floor(Math.random() * 4)],
213 windSpeed: Math.round(Math.random() * 20) + 5,
214 humidity: Math.round(Math.random() * 40) + 40,
215 visibility: Math.round(Math.random() * 10) + 5,
216 coordinates: { lat: 51.5074, lon: -0.1278 }
217 };
218}
219
220// Main App Component
221function App() {
222 return (
223 <div className="app">
224 <WeatherDashboard />
225 </div>
226 );
227}This weather dashboard demonstrates multiple useEffect patterns:
Multiple Coordinated Effects:
Dependency Management:
[location] - Refetch when location changes[weather] - Run when weather data updates[] - Run once for global listeners[weather, location] - Update title when either changesCleanup Patterns:
clearIntervalremoveEventListenerConditional Effects:
Real-World Patterns:
You've mastered useEffect for side effects! Next, you'll learn about handling user input:
You'll create interactive forms that handle user input efficiently and provide great user experiences.
Your progress is saved in this browser, on this device only.