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 dependency
function 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>;
}
// ✅ Correct
function 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 loop
function BadExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // This will run forever!
}, [count]);
return <div>{count}</div>;
}
// ✅ Use functional update or different approach
function 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 };
}
// Usage
function 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!

Share Feedback