After watching myself struggle with useEffect, let me break down what's actually happening under the hood.
The Lifecycles You Need to Know
Think of your component like a house:
- Mounting = Building the house (component first appears)
- Rendering = Redecorating (component updates)
- Unmounting = Demolishing (component disappears)
Here's how useEffect works with each:
function App() {
useEffect(() => {
console.log('🏗️ House built! (Mounted)');
return () => {
console.log('🏚️ House demolished! (Unmounted)');
};
}, []); // Empty array = only on mount/unmount
useEffect(() => {
console.log('🎨 Redecorating! (Re-rendered)');
}); // No array = every render
return <div>Hello!</div>;
}
When Effects Actually Run
Here's what happens in real life:
function RoomLight({ isOn }) {
// 1. Runs after mount AND when isOn changes
useEffect(() => {
console.log(`Light turned ${isOn ? 'on' : 'off'}`);
}, [isOn]);
// 2. Runs after EVERY render
useEffect(() => {
console.log('Room redecorated');
}); // No dependency array
// 3. Runs ONCE after mount
useEffect(() => {
console.log('Room built');
}, []); // Empty dependency array
return <div>Room</div>;
}
Cleanup (The Important Part)
Here's when you need cleanup:
- Timers
- Subscriptions
- Event listeners
- WebSocket connections
function Timer() {
useEffect(() => {
// Set up
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
// Clean up
return () => {
clearInterval(timer); // Prevents memory leaks
};
}, []); // Only on mount
}
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// Set up
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Clean up
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Only on mount
}
Common Gotchas I Hit
1. Missing Dependencies
function Counter({ start }) {
const [count, setCount] = useState(start);
// Wrong - doesn't update when start changes
useEffect(() => {
setCount(start);
}, []); // Lint will warn you
// Right - updates when start changes
useEffect(() => {
setCount(start);
}, [start]);
}
2. Running Too Often
function Profile({ user }) {
// Bad - new object every render
useEffect(() => {
console.log('Profile updated');
}, [{ name: user.name }]); // Runs every time!
// Good - only when name changes
useEffect(() => {
console.log('Profile updated');
}, [user.name]);
}
Quick Tips
- The cleanup function runs before the effect runs again
- Dependencies should include everything that changes
- Empty array = mount/unmount only
- No array = every render
- When in doubt, let the linter guide you
Mental Model
Think of it this way:
useEffect(() => {
// This runs AFTER render
console.log('Effect happened');
return () => {
// This runs BEFORE next effect or unmount
console.log('Cleanup happened');
};
}, [/* dependencies change = effect runs again */]);
That's useEffect stripped down to what matters. Still confused about something? Drop a comment - I love debugging these things.
Follow for more React tips from the trenches 🎯