useEffect

Understanding useEffect: React's "Do Something After Render" Hook
Hey! Now let's learn about useEffect - it's like giving your component superpowers to do things at specific times.
What Does useEffect Do?
Think of useEffect as a side effect manager. It lets your component:
Fetch data when it first loads
Update something when data changes
Clean up when the component disappears
Listen to events (like window resizing)
The Basic Pattern
useEffect(() => {
// Code that runs after render
}, [dependencies]); // Optional dependency array
Real-Life Example: A Page Title Updater
Imagine you want to change your browser tab title whenever something changes in your app:
import React, { useState, useEffect } from 'react';
function PageTitleUpdater() {
const [pageName, setPageName] = useState('Home');
// EFFECT 1: Run once when component loads
useEffect(() => {
console.log('Component loaded!');
document.title = `My App - ${pageName}`;
}, []); // Empty array = run once
// EFFECT 2: Run whenever pageName changes
useEffect(() => {
console.log('Page name changed to:', pageName);
document.title = `My App - ${pageName}`;
}, [pageName]); // Run when pageName changes
return (
<div>
<h1>Welcome to {pageName} Page</h1>
<button onClick={() => setPageName('Home')}>Home</button>
<button onClick={() => setPageName('About')}>About</button>
<button onClick={() => setPageName('Contact')}>Contact</button>
</div>
);
}
Three Main Ways to Use useEffect
1. Run Once (On Mount)
useEffect(() => {
// This runs ONCE when component first appears
console.log('Component loaded for the first time');
// Common uses:
// - Fetch initial data
// - Set up timers
// - Connect to WebSocket
}, []); // ← Empty brackets mean "run once"
Example: Fetch data when page loads
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user data when component loads
fetch('https://api.example.com/user')
.then(response => response.json())
.then(data => setUser(data));
console.log('Data fetch started');
}, []); // Runs once
return (
<div>
{user ? (
<p>Welcome, {user.name}!</p>
) : (
<p>Loading user data...</p>
)}
</div>
);
}
2. Run When Something Changes
useEffect(() => {
// This runs whenever 'count' changes
console.log(`Count changed to: ${count}`);
}, [count]); // ← Put variables to watch here
Example: Save form data when it changes
function AutoSaveForm() {
const [formData, setFormData] = useState({
title: '',
content: ''
});
// Save whenever formData changes
useEffect(() => {
console.log('Saving form data:', formData);
// In real app, you might:
// 1. Save to localStorage
localStorage.setItem('draft', JSON.stringify(formData));
// 2. Or send to API
// api.saveDraft(formData);
}, [formData]); // Runs when formData changes
return (
<div>
<input
placeholder="Title"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
/>
<textarea
placeholder="Content"
value={formData.content}
onChange={(e) => setFormData({...formData, content: e.target.value})}
/>
</div>
);
}
3. Cleanup (Prevent Memory Leaks)
useEffect(() => {
// Setup code here
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
// Cleanup function (runs before next effect OR when component unmounts)
return () => {
console.log('Cleaning up...');
clearInterval(interval);
};
}, []);
Example: Live Clock with Cleanup
function LiveClock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
console.log('Clock started');
// Start timer
const timerId = setInterval(() => {
setTime(new Date());
}, 1000);
// Cleanup function
return () => {
console.log('Clock stopped');
clearInterval(timerId);
};
}, []); // Empty array = run once and clean up once
return (
<div>
<h2>Current Time: {time.toLocaleTimeString()}</h2>
</div>
);
}
Common Real-World Examples
Example 1: Window Event Listener
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
console.log('Setting up resize listener');
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// Add event listener
window.addEventListener('resize', handleResize);
// Cleanup: Remove event listener
return () => {
console.log('Removing resize listener');
window.removeEventListener('resize', handleResize);
};
}, []); // Run once on mount
return (
<div>
<p>Window Size: {windowSize.width} x {windowSize.height}</p>
<p>{windowSize.width < 768 ? 'Mobile' : 'Desktop'} view</p>
</div>
);
}
Example 2: API Data Fetching with Loading States
function WeatherApp() {
const [city, setCity] = useState('London');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
// Fetch weather whenever city changes
useEffect(() => {
let isMounted = true; // Prevent state updates if component unmounts
const fetchWeather = async () => {
setLoading(true);
try {
const response = await fetch(
`https://api.weather.com/${city}`
);
const data = await response.json();
// Only update state if component is still mounted
if (isMounted) {
setWeather(data);
}
} catch (error) {
if (isMounted) {
console.error('Failed to fetch weather:', error);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchWeather();
// Cleanup function
return () => {
isMounted = false;
};
}, [city]); // Re-fetch when city changes
return (
<div>
<input
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Enter city"
/>
{loading ? (
<p>Loading weather for {city}...</p>
) : weather ? (
<div>
<h3>Weather in {city}</h3>
<p>Temperature: {weather.temp}°C</p>
<p>Condition: {weather.condition}</p>
</div>
) : (
<p>Enter a city to see weather</p>
)}
</div>
);
}
Example 3: Simple Chat Notification
function ChatApp() {
const [messages, setMessages] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
// When messages change, update unread count
useEffect(() => {
setUnreadCount(messages.length);
// Show notification for new messages
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
console.log(`New message: ${lastMessage.text}`);
// Browser notification (if permitted)
if (Notification.permission === 'granted') {
new Notification('New Message', {
body: lastMessage.text
});
}
}
}, [messages]); // Run when messages array changes
const addMessage = (text) => {
setMessages([...messages, { text, time: new Date() }]);
};
return (
<div>
<h2>Chat ({unreadCount} messages)</h2>
<button onClick={() => addMessage('Hello!')}>
Add Test Message
</button>
<button onClick={() => addMessage('How are you?')}>
Add Another
</button>
<div>
{messages.map((msg, index) => (
<p key={index}>{msg.text}</p>
))}
</div>
</div>
);
}
The Dependency Array: When Effects Run
// 1. No array = runs after EVERY render (careful!)
useEffect(() => {
console.log('Runs after every single render');
});
// 2. Empty array = runs ONCE after first render
useEffect(() => {
console.log('Runs once when component mounts');
}, []);
// 3. With dependencies = runs when those values change
useEffect(() => {
console.log('Runs when count OR text changes');
}, [count, text]);
Common Mistakes to Avoid
Mistake 1: Missing dependencies
const [count, setCount] = useState(0);
const [result, setResult] = useState(0);
// ❌ WRONG: Forgot count in dependencies
useEffect(() => {
setResult(count * 2);
}, []); // This won't update when count changes!
// ✅ CORRECT: Include all used variables
useEffect(() => {
setResult(count * 2);
}, [count]); // Now it updates when count changes
Mistake 2: Infinite loops
const [count, setCount] = useState(0);
// ❌ INFINITE LOOP: Updates state, triggers effect, updates state...
useEffect(() => {
setCount(count + 1); // Bad!
}, [count]);
// ✅ Solution: Add condition or fix logic
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
}, [count]);
Mistake 3: Forgetting cleanup
// ❌ MEMORY LEAK: Event listener not removed
useEffect(() => {
window.addEventListener('scroll', handleScroll);
}, []);
// ✅ CORRECT: Always clean up
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
Practice Exercise: Build a Stopwatch
Try creating a stopwatch that:
Starts counting when you click "Start"
Stops when you click "Stop"
Resets when you click "Reset"
Shows the elapsed time
function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
// Your useEffect goes here!
// Hint: You'll need setInterval and cleanup
return (
<div>
<h1>{time} seconds</h1>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => setIsRunning(false)}>Stop</button>
<button onClick={() => setTime(0)}>Reset</button>
</div>
);
}
When to Use useEffect?
✅ DO use useEffect for:
Fetching data on component load
Setting up subscriptions (WebSocket, event listeners)
Updating DOM in response to state changes
Cleaning up resources
❌ DON'T use useEffect for:
Event handlers (use onClick instead)
Derived state (calculate directly in render)
Synchronizing state that should update together
Summary Cheat Sheet
// 1. Run once on mount (like componentDidMount)
useEffect(() => {
// Initial setup here
}, []);
// 2. Run when dependencies change (like componentDidUpdate)
useEffect(() => {
// Update logic here
}, [dep1, dep2]);
// 3. Cleanup (like componentWillUnmount)
useEffect(() => {
// Setup
return () => {
// Cleanup
};
}, []);
// 4. Run on every render (use sparingly!)
useEffect(() => {
// Runs after every render
});
Remember: useEffect is your tool for "side effects" - anything that happens after render. Start simple, add dependencies carefully, and always clean up!