useEffect Hook - Managing Side Effects
Understanding Side Effects in React
Side effects are operations that affect something outside the scope of the component function being executed. Common examples include data fetching, subscriptions, timers, manually changing the DOM, and logging.
The useEffect hook lets you perform side effects in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in class components, but unified into a single API.
Why Side Effects Need Special Handling
React components should be pure functions - given the same props and state, they should render the same UI. Side effects break this purity, so React provides useEffect to handle them predictably.
Basic useEffect Syntax
The useEffect hook takes two arguments: a function containing your side effect code, and an optional dependency array.
import React, { useState, useEffect } from 'react';
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
// Effect runs after every render
useEffect(() => {
document.title = `Count: ${count}`;
console.log('Effect executed');
});
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
export default DocumentTitleUpdater;
In this example, the effect runs after every render, updating the document title to reflect the current count.
The Dependency Array
The second argument to useEffect is a dependency array that controls when the effect runs:
- No dependency array: Effect runs after every render
- Empty array []: Effect runs only once after initial render
- Array with values: Effect runs only when those values change
import React, { useState, useEffect } from 'react';
function DependencyDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
// Runs only on mount (once)
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs when count changes
useEffect(() => {
console.log(`Count changed to: ${count}`);
}, [count]);
// Runs when name changes
useEffect(() => {
console.log(`Name changed to: ${name}`);
}, [name]);
// Runs when either count or name changes
useEffect(() => {
console.log(`Count: ${count}, Name: ${name}`);
}, [count, name]);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<h2>Name: {name}</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
export default DependencyDemo;
Common Mistake: Missing Dependencies
Always include all values from the component scope that are used inside the effect in the dependency array. Omitting dependencies can lead to stale data and bugs. Use ESLint plugin eslint-plugin-react-hooks to catch these issues.
Cleanup Functions
Effects can optionally return a cleanup function that React will run before the component unmounts or before running the effect again. This is essential for preventing memory leaks.
import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(true);
useEffect(() => {
if (!isRunning) return;
// Setup: Create interval
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup: Clear interval when component unmounts
// or when dependencies change
return () => {
console.log('Cleaning up interval');
clearInterval(intervalId);
};
}, [isRunning]); // Re-run effect when isRunning changes
return (
<div>
<h2>Seconds Elapsed: {seconds}</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Resume'}
</button>
</div>
);
}
export default TimerComponent;
When to Use Cleanup
Use cleanup functions for: subscriptions, timers (setTimeout/setInterval), event listeners, WebSocket connections, and any other resources that need manual cleanup.
Fetching Data with useEffect
One of the most common use cases for useEffect is fetching data from an API when the component mounts or when certain values change.
import React, { useState, useEffect } from 'react';
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);
// Fetch user data
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Username: {user.username}</p>
</div>
);
}
export default UserProfile;
Async Functions in useEffect
You cannot make the effect callback itself async, but you can define and call an async function inside it:
import React, { useState, useEffect } from 'react';
function PostsList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Define async function inside effect
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data.slice(0, 5)); // Get first 5 posts
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // Empty array - fetch once on mount
if (loading) {
return <div>Loading posts...</div>;
}
return (
<div>
<h2>Recent Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
</div>
);
}
export default PostsList;
Common useEffect Patterns
1. Event Listeners
function WindowSize() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup: Remove event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // No dependencies - setup once
return <div>Window width: {windowWidth}px</div>;
}
2. Debouncing Search Input
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// Debounce: Wait 500ms after user stops typing
const timerId = setTimeout(() => {
if (searchTerm) {
fetch(`https://api.example.com/search?q=${searchTerm}`)
.then(res => res.json())
.then(data => setResults(data));
} else {
setResults([]);
}
}, 500);
// Cleanup: Cancel previous timer
return () => clearTimeout(timerId);
}, [searchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
3. Local Storage Sync
function ThemeSelector() {
const [theme, setTheme] = useState(() => {
// Initialize from localStorage
return localStorage.getItem('theme') || 'light';
});
useEffect(() => {
// Sync to localStorage whenever theme changes
localStorage.setItem('theme', theme);
document.body.className = theme;
}, [theme]);
return (
<div>
<button onClick={() => setTheme('light')}>Light</button>
<button onClick={() => setTheme('dark')}>Dark</button>
<p>Current theme: {theme}</p>
</div>
);
}
Exercise 1: Live Clock
Create a LiveClock component that displays the current time and updates every second. The clock should stop updating when the component unmounts.
// Requirements:
// 1. Display time in HH:MM:SS format
// 2. Update every second using setInterval
// 3. Clean up interval on unmount
// 4. Add a "Stop" button to pause updates
Exercise 2: User Search
Build a UserSearch component that fetches users from https://jsonplaceholder.typicode.com/users and filters them based on search input.
// Requirements:
// 1. Fetch users on component mount
// 2. Filter users by name as user types
// 3. Debounce the filter by 300ms
// 4. Show loading state while fetching
Exercise 3: Mouse Tracker
Create a MouseTracker component that displays the current mouse position (x, y coordinates) and updates as the mouse moves.
// Requirements:
// 1. Track mousemove events on window
// 2. Display X and Y coordinates
// 3. Clean up event listener on unmount
// 4. Throttle updates to every 100ms for performance
Summary
useEffecthandles side effects in function components- Dependency array controls when effects run
- Return cleanup functions to prevent memory leaks
- Common patterns: data fetching, subscriptions, timers, event listeners
- Use async functions inside effects, not as the effect callback itself
- Always include all dependencies to avoid stale closures