Lesson 3: useEffect and Side Effects
Master the useEffect hook to handle side effects, API calls, and component lifecycle in React applications
Lesson 3: useEffect and Side Effects π»
Introduction
Welcome back! In the previous lessons, you learned about React Hooks basics and how to manage state with useState. Now it's time to tackle one of the most powerful and essential hooks: useEffect. This hook opens the door to handling side effects in your React componentsβeverything from fetching data from APIs to setting up subscriptions, manipulating the DOM directly, and managing timers.
π€ Did you know? Before hooks, handling side effects required class components with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. The useEffect hook elegantly combines all three into a single, intuitive API!
What Are Side Effects? π
Before diving into useEffect, let's clarify what we mean by side effects. In programming, a side effect is any operation that affects something outside the scope of the function being executed.
Common Side Effects in React:
- π‘ Fetching data from an API
- π Setting up subscriptions (WebSockets, event listeners)
- β° Timers and intervals (setTimeout, setInterval)
- π Directly manipulating the DOM (changing document.title)
- πΎ Reading/writing to localStorage
- π Logging or analytics
The Pure Function Principle
React components should ideally be pure functions during renderingβgiven the same props and state, they should return the same JSX. Side effects break this purity, which is why we need a dedicated place to handle them.
βββββββββββββββββββββββββββββββββββββββ
β COMPONENT RENDERING CYCLE β
βββββββββββββββββββββββββββββββββββββββ€
β β
β Props + State β Pure Function β
β β β
β JSX β
β β
β Side Effects β useEffect Hook β
β β
βββββββββββββββββββββββββββββββββββββββ
π‘ Think of it this way: Your component's render function is like cooking a mealβit transforms ingredients (props/state) into a finished dish (JSX). Side effects are like cleaning up afterward or ordering more ingredientsβnecessary, but separate from the cooking itself!
The useEffect Hook: Core Syntax π―
The useEffect hook accepts two arguments:
useEffect(() => {
// Side effect code goes here
return () => {
// Cleanup function (optional)
};
}, [dependencies]);
Breaking Down the Anatomy:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β useEffect(effectFunction, dependencyArray) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β βββ When to run
β
βββ What to run
Effect Function:
βββββββββββββββββββββββββββββββ
β () => { β
β // Execute side effect β
β return () => { β
β // Cleanup β
β }; β
β } β
βββββββββββββββββββββββββββββββ
Dependency Array:
ββββββββββββββββββββββββββββββββββββββββββββ
β [] β Run once (mount only) β
β [a, b] β Run when a or b changes β
β (undefined) β Run after every render β
ββββββββββββββββββββββββββββββββββββββββββββ
Understanding the Dependency Array π²
The dependency array is crucialβit determines when your effect runs. This is where many beginners struggle, so let's break it down thoroughly.
Three Scenarios:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dependency Array Behavior β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ€
β Pattern β When Effect Runs β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β useEffect(fn) β After EVERY render β
β β β οΈ Usually too often! β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β useEffect(fn,[])β Only on mount (first render) β
β β β
Good for initial setup β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β useEffect(fn, β On mount + when dependencies change β
β [a, b]) β β
Most common pattern β
ββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ
Example: Count Tracking
import { useState, useEffect } from 'react';
function CounterLogger() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Guest');
// Runs after EVERY render
useEffect(() => {
console.log('Component rendered!');
});
// Runs only on mount
useEffect(() => {
console.log('Component mounted!');
}, []);
// Runs when count changes
useEffect(() => {
console.log(`Count changed to: ${count}`);
}, [count]);
// Runs when count OR name changes
useEffect(() => {
console.log(`Either count or name changed!`);
}, [count, name]);
return (
<div>
<p>{name}: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setName('User')}>Change Name</button>
</div>
);
}
π‘ Tip: Always include all values from the component scope that your effect uses in the dependency array. Modern React linters will warn you if you forget!
Real-World Examples π
Example 1: Fetching Data from an API π‘
One of the most common use cases for useEffect is fetching data when a component mounts.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset states 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>
</div>
);
}
What's happening here?
- When the component mounts or
userIdchanges, the effect runs - We set
loadingto true and clear any previous errors - We fetch the user data from the API
- On success, we update the
userstate and setloadingto false - On error, we capture the error message
- The component re-renders based on the new state
ββββββββββββββββββββββββββββββββββββββββββ
β Data Fetching Flow β
ββββββββββββββββββββββββββββββββββββββββββ€
β β
β Component Mounts β
β β β
β useEffect Triggers β
β β β
β setLoading(true) β
β β β
β API Request β
β β β
β ββββββ΄βββββ β
β Success Error β
β β β β
β setUser setError β
β β β β
β ββββββ¬βββββ β
β β β
β setLoading(false) β
β β β
β Re-render with Data β
β β
ββββββββββββββββββββββββββββββββββββββββββ
Example 2: Cleanup Functions π§Ή
Some side effects need cleanup to prevent memory leaks. The cleanup function runs when:
- The component unmounts
- Before the effect runs again (if dependencies changed)
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(true);
useEffect(() => {
if (!isRunning) return; // Don't set up interval if not running
// Set up the interval
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function: runs before next effect or unmount
return () => {
clearInterval(intervalId);
console.log('Interval cleared!');
};
}, [isRunning]); // Re-run effect when isRunning changes
return (
<div>
<h2>Timer: {seconds} seconds</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Resume'}
</button>
</div>
);
}
Why cleanup matters:
Without the cleanup function, every time isRunning changes, a new interval would be created without clearing the old one. This causes:
- β οΈ Multiple intervals running simultaneously
- β οΈ Memory leaks
- β οΈ Unpredictable behavior
Example 3: Document Title Updates π
A simple but practical example: updating the browser tab title based on component state.
import { useState, useEffect } from 'react';
function MessageCounter() {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Update document title
if (messages.length === 0) {
document.title = 'No new messages';
} else {
document.title = `(${messages.length}) New Messages`;
}
// Cleanup: reset title when component unmounts
return () => {
document.title = 'My App';
};
}, [messages.length]); // Only re-run when message count changes
const addMessage = () => {
setMessages([...messages, `Message ${messages.length + 1}`]);
};
return (
<div>
<h2>You have {messages.length} messages</h2>
<button onClick={addMessage}>Add Message</button>
<ul>
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
</div>
);
}
Example 4: Event Listeners π―
Setting up and cleaning up event listeners is another common pattern.
import { useState, useEffect } from 'react';
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// Event handler function
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
// Add event listener
window.addEventListener('mousemove', handleMouseMove);
// Cleanup: remove event listener
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty array: setup once on mount, cleanup on unmount
return (
<div>
<h2>Mouse Position</h2>
<p>X: {position.x}, Y: {position.y}</p>
</div>
);
}
Key Pattern: Notice how we:
- Define the handler function inside the effect
- Add the event listener
- Return a cleanup function that removes the listener
- Use an empty dependency array since this setup doesn't depend on any props or state
Common Mistakes and How to Avoid Them β οΈ
Mistake 1: Missing Dependencies
// β WRONG: Missing userId in dependencies
function BadExample({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Bug: won't re-fetch when userId changes!
return <div>{user?.name}</div>;
}
// β
CORRECT: Include all dependencies
function GoodExample({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Now re-fetches when userId changes
return <div>{user?.name}</div>;
}
π‘ Rule of thumb: If your effect uses a value from props or state, include it in the dependency array!
Mistake 2: Infinite Loops π
// β WRONG: Creates infinite loop
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Changes count...
}, [count]); // ...which triggers effect again!
return <div>{count}</div>;
}
// β
CORRECT: Use empty array or different logic
function GoodCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
// Runs only once on mount
const timer = setTimeout(() => setCount(1), 1000);
return () => clearTimeout(timer);
}, []);
return <div>{count}</div>;
}
Mistake 3: Not Cleaning Up Resources
// β WRONG: Memory leak
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(c => c + 1);
}, 1000);
// No cleanup! Interval keeps running even after unmount
}, []);
return <div>{count}</div>;
}
// β
CORRECT: Proper cleanup
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id); // Clean up!
}, []);
return <div>{count}</div>;
}
Mistake 4: Async Functions in useEffect
// β WRONG: Can't make effect function async directly
function BadAsync() {
const [data, setData] = useState(null);
useEffect(async () => { // β Error!
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
}, []);
return <div>{data}</div>;
}
// β
CORRECT: Define async function inside
function GoodAsync() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
};
fetchData();
}, []);
return <div>{data}</div>;
}
// β
ALSO CORRECT: Use .then() instead
function AlsoGoodAsync() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
return <div>{data}</div>;
}
Why? The effect function must return either nothing or a cleanup function, not a Promise!
Advanced Pattern: Cancelling Requests π«
When fetching data, the component might unmount before the request completes. This can cause warnings about updating unmounted components.
function SafeDataFetcher({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // Flag to track if effect was cleaned up
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) { // Only update if still mounted
setUser(data);
setLoading(false);
}
});
return () => {
cancelled = true; // Set flag on cleanup
};
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
π§ Modern approach: Use the AbortController API:
function ModernDataFetcher({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, {
signal: controller.signal
})
.then(res => res.json())
.then(setUser)
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
});
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}
When to Use useEffect vs. Other Solutions π€
Not everything needs useEffect! Here's a decision guide:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Do you need useEffect? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Calculating values from props/state? β
β β NO: Just use regular variables β
β β
β Updating state based on props? β
β β MAYBE: Consider if logic can be in render β
β β
β Fetching data on mount/prop change? β
β β YES: useEffect is appropriate β
β β
β Subscribing to external events? β
β β YES: useEffect with cleanup β
β β
β Responding to user events (clicks, etc.)? β
β β NO: Use event handlers instead β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Example: Don't Use Effect When You Don't Need It
// β Unnecessary useEffect
function BadFullName({ firstName, lastName }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
// β
Just compute it directly
function GoodFullName({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <div>{fullName}</div>;
}
Key Takeaways π―
β useEffect handles side effects that don't belong in the render logic
β The dependency array controls when effects run:
- No array: every render
- Empty array: only on mount
- With values: when those values change
β Always clean up resources like intervals, listeners, and subscriptions
β Include all dependencies your effect uses from props/state
β Can't use async directly in the effect functionβdefine async function inside
β Avoid infinite loops by carefully managing dependencies
β Not everything needs useEffectβcalculate derived values directly in render
β Cleanup functions run before the next effect and on unmount
π§ Try This: Practice Exercise
Create a simple component that:
- Fetches a random joke from an API when mounted
- Has a "Get New Joke" button that fetches a new joke
- Shows loading state while fetching
- Updates the document title with the joke setup
- Cleans up by resetting the title on unmount
API endpoint: https://official-joke-api.appspot.com/random_joke
Quick Reference Card π
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β useEffect CHEAT SHEET β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β β
β Basic Syntax: β
β useEffect(() => { β
β // effect code β
β return () => { /* cleanup */ }; β
β }, [dependencies]); β
β β
β Common Patterns: β
β β’ Data fetching: [], [userId] β
β β’ Subscriptions: [] with cleanup β
β β’ Timers: [] with clearInterval/Timeout β
β β’ Event listeners: [] with removeEventListener β
β β’ Document updates: [value] to sync β
β β
β Remember: β
β β List all dependencies β
β β Clean up resources β
β β Handle async properly β
β β Don't modify state that's in dependencies β
β β Don't use async as the effect function β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Further Study
- React Official Docs: useEffect
- A Complete Guide to useEffect by Dan Abramov
- React useEffect Hook Tutorial
In the next lesson, we'll explore useContext and learn how to share state across multiple components without prop drilling. See you there! π