useEffect - Side Effects and Lifecycle
The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined in class components.
Basic Syntax
import React, { useState, useEffect } from 'react';
function MyComponent() { const [count, setCount] = useState(0);
// Effect runs after every render useEffect(() => { document.title = `Count: ${count}`; });
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </div> );}Effect with Dependencies
Use the dependency array to control when effects run:
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { async function fetchUser() { setLoading(true); try { const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); setUser(userData); } catch (error) { console.error('Failed to fetch user:', error); } finally { setLoading(false); } }
fetchUser(); }, [userId]); // Only re-run when userId changes
if (loading) return <div>Loading...</div>; if (!user) return <div>User not found</div>;
return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );}Cleanup Functions
Always clean up subscriptions, timers, and event listeners:
function Timer() { const [seconds, setSeconds] = useState(0);
useEffect(() => { const interval = setInterval(() => { setSeconds(prev => prev + 1); }, 1000);
// Cleanup function return () => { clearInterval(interval); }; }, []); // Empty dependency array = run once
return <div>Seconds: {seconds}</div>;}Common Patterns
API Data Fetching
function PostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let isCancelled = false;
async function fetchPosts() { try { setLoading(true); const response = await fetch('/api/posts'); const data = await response.json();
if (!isCancelled) { setPosts(data); setError(null); } } catch (err) { if (!isCancelled) { setError(err.message); } } finally { if (!isCancelled) { setLoading(false); } } }
fetchPosts();
return () => { isCancelled = true; }; }, []);
if (loading) return <div>Loading posts...</div>; if (error) return <div>Error: {error}</div>;
return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> );}Event Listeners
function WindowSize() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }
window.addEventListener('resize', handleResize);
// Cleanup return () => { window.removeEventListener('resize', handleResize); }; }, []);
return ( <div> Window size: {windowSize.width} x {windowSize.height} </div> );}Dependency Array Rules
Empty Array []
Effect runs only once after initial render:
useEffect(() => { // Runs once on mount console.log('Component mounted');
return () => { // Runs once on unmount console.log('Component unmounted'); };}, []);No Dependency Array
Effect runs after every render:
useEffect(() => { // Runs after every render console.log('Component rendered');});With Dependencies
Effect runs when dependencies change:
useEffect(() => { // Runs when count or name changes console.log(`Count: ${count}, Name: ${name}`);}, [count, name]);Best Practices
1. Always Include Dependencies
function SearchResults({ query }) { const [results, setResults] = useState([]);
useEffect(() => { async function search() { const response = await fetch(`/api/search?q=${query}`); const data = await response.json(); setResults(data); }
search(); }, [query]); // Include query in dependencies
return ( <ul> {results.map(result => ( <li key={result.id}>{result.title}</li> ))} </ul> );}2. Separate Concerns with Multiple Effects
function UserDashboard({ userId }) { const [user, setUser] = useState(null); const [posts, setPosts] = useState([]);
// Effect for user data useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
// Effect for posts data useEffect(() => { fetchUserPosts(userId).then(setPosts); }, [userId]);
// Effect for document title useEffect(() => { if (user) { document.title = `${user.name}'s Dashboard`; } }, [user]);
return ( <div> {user && <h1>Welcome, {user.name}!</h1>} {posts.map(post => ( <div key={post.id}>{post.title}</div> ))} </div> );}Common Mistakes
Missing Dependencies
// ❌ Missing dependencyfunction BadExample({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(userId).then(setUser); // userId is used but not in deps }, []); // This will only run once!
return <div>{user?.name}</div>;}
// ✅ Correctfunction GoodExample({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); // Include userId
return <div>{user?.name}</div>;}Infinite Loops
// ❌ This creates an infinite loopfunction BadExample() { const [count, setCount] = useState(0);
useEffect(() => { setCount(count + 1); // This will run forever! }, [count]);
return <div>{count}</div>;}
// ✅ Use functional update or different approachfunction GoodExample() { const [count, setCount] = useState(0);
useEffect(() => { const timer = setTimeout(() => { setCount(prev => prev + 1); }, 1000);
return () => clearTimeout(timer); }, []); // Empty deps, runs once
return <div>{count}</div>;}Advanced Patterns
Custom Hook for Data Fetching
function useApi(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let isCancelled = false;
async function fetchData() { try { setLoading(true); const response = await fetch(url); const result = await response.json();
if (!isCancelled) { setData(result); setError(null); } } catch (err) { if (!isCancelled) { setError(err); } } finally { if (!isCancelled) { setLoading(false); } } }
fetchData();
return () => { isCancelled = true; }; }, [url]);
return { data, loading, error };}
// Usagefunction UserProfile({ userId }) { const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {user.name}!</div>;}Next Steps
Understanding useEffect is crucial for handling side effects in React. Next, we’ll explore useContext for sharing state across components without prop drilling.
The combination of useState and useEffect covers most component logic needs, but useContext will help you manage global state more elegantly!