Chapter 2: useEffect
This chapter is crucial. useEffect is the most complex hook to master, but also the most used after useState.
What is a Side Effect?
A side effect is any operation that affects something outside the component:
- Calling an API
- Modifying the page title
- Subscribing to an event (WebSocket, resize, scroll)
- Direct DOM manipulation
- Using a timer (setTimeout, setInterval)
These operations cannot be done directly in the component body, as it runs on every render.
Basic Syntax
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Code runs after render
console.log('Component rendered!');
});
return <div>My component</div>;
}
The Dependency Array
The second argument of useEffect controls when the effect runs:
No array: on every render
useEffect(() => {
console.log('Runs after EVERY render');
});
Rarely useful - can cause performance issues.
Empty array: once only
useEffect(() => {
console.log('Runs ONCE after first render');
}, []);
Equivalent to componentDidMount in classes.
With dependencies: when they change
useEffect(() => {
console.log('Runs when userId changes');
fetchUser(userId);
}, [userId]);
The effect runs:
- After the first render
- After every render where
userIdhas changed
Concrete Example: API Call
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Async function in useEffect
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return null;
return <div>{user.name}</div>;
}
Cleanup Function
useEffect can return a function that will be called:
- Before the next effect execution
- When the component unmounts
useEffect(() => {
// Setup
const subscription = api.subscribe(data => {
setData(data);
});
// Cleanup
return () => {
subscription.unsubscribe();
};
}, []);
Example: Resize Event
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup: remove listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // [] because we want to subscribe only once
return <p>{size.width} x {size.height}</p>;
}
Example: Timer
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: stop the timer
return () => {
clearInterval(interval);
};
}, []);
return <p>{seconds} seconds</p>;
}
Common Pitfalls
1. Infinite Loop
// DANGER! Infinite loop
useEffect(() => {
setCount(count + 1); // Modifies state
}); // No array = runs on every render
// Modifies state -> re-render -> useEffect -> modifies state -> ...
Solution: always add a dependency array.
2. Missing Dependencies
// PROBLEM: userId is missing from dependencies
useEffect(() => {
fetchUser(userId);
}, []); // Fetches only on mount, ignores userId changes
Solution: add all used values to the array.
useEffect(() => {
fetchUser(userId);
}, [userId]); // Correct
3. Object or Function in Dependencies
// PROBLEM: object is recreated on every render
function MyComponent({ user }) {
const options = { limit: 10 }; // New object on every render
useEffect(() => {
fetchData(options);
}, [options]); // options changes on every render!
}
Solution: move outside the component or use useMemo (next module).
4. Async in useEffect
// INCORRECT - useEffect cannot be async
useEffect(async () => { // Error!
const data = await fetchData();
}, []);
// CORRECT - define async function inside
useEffect(() => {
const loadData = async () => {
const data = await fetchData();
setData(data);
};
loadData();
}, []);
Execution Order
function MyComponent() {
console.log('1. Render');
useEffect(() => {
console.log('3. Effect');
return () => console.log('2. Cleanup (if re-render)');
});
return <div>Test</div>;
}
// First render:
// 1. Render
// 3. Effect
// Re-render:
// 1. Render
// 2. Cleanup
// 3. Effect
Comparison with PHP Lifecycle
| Event | PHP | React useEffect |
|---|---|---|
| Initialization | constructor | useEffect(() => {}, []) |
| Every request | controller code | useEffect(() => {}) |
| Cleanup | destructor | function returned by useEffect |
When to Use useEffect?
YES:
- API calls
- Subscriptions (WebSocket, events)
- Timers
- Direct DOM manipulation
- Synchronizing with external systems
NO:
- Computing a derived value from state -> useMemo
- Transforming data for rendering -> do it in render
- Reacting to user action -> event handler
Exercises
Exercise 1: Dynamic Page Title
Create a component that updates the page title with the counter.
Solution:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Counter: ${count}`;
}, [count]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Exercise 2: Fetch with Error Handling
Create a component that loads a list of posts from an API.
Solution:
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) throw new Error('Network error');
const data = await response.json();
setPosts(data.slice(0, 10));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Key Points to Remember
- useEffect runs code after rendering
- Empty array
[]= once on mount - With dependencies = when these values change
- Return function = cleanup
- Always list dependencies used in the effect
- Async: define async function inside, not on useEffect
<- Previous chapter | Back to module | Next chapter: useRef ->