useEffect and Side Effects
While state and props handle data within React, many applications need to interact with the outside world - fetching data, setting up subscriptions, or manipulating the DOM directly. These are called side effects.
What are Side Effects?
Side effects are anything that affects something outside the scope of the current function:
- Fetching data from an API
- Subscriptions to external data sources
- Timers (setTimeout, setInterval)
- Logging to the console
- Directly manipulating the DOM
- Storing data in localStorage
The useEffect Hook
React provides useEffect to handle side effects:
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Empty array = run once
return <p>Seconds: {seconds}</p>;
}
Dependency Array
The second argument controls when the effect runs:
No Dependency Array - Runs Every Render
useEffect(() => {
console.log('Runs after every render');
});
Empty Array - Runs Once
useEffect(() => {
console.log('Runs only after initial render');
}, []);
With Dependencies - Runs When Dependencies Change
useEffect(() => {
console.log(`User ID changed to: ${userId}`);
fetchUserData(userId);
}, [userId]); // Runs when userId changes
Fetching Data
A common use case for useEffect:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
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>;
return <div>Hello, {user.name}!</div>;
}
Cleanup Function
The function returned from useEffect runs when:
- The component unmounts
- Before the effect re-runs (when dependencies change)
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// Cleanup: disconnect when leaving room
return () => {
connection.disconnect();
};
}, [roomId]);
return <h1>Room: {roomId}</h1>;
}
Why Cleanup Matters
Without cleanup, you might:
- Have memory leaks from subscriptions
- Make unnecessary API calls
- Have multiple intervals running
- Listen to events multiple times
// ❌ Memory leak - no cleanup
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
// ✅ Proper cleanup
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
Common Patterns
Document Title
function Page({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}
Local Storage
function Settings() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme ({theme})
</button>
);
}
Event Listeners
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <p>Scroll position: {scrollY}px</p>;
}
Rules of useEffect
1. Don't Lie About Dependencies
// ❌ Missing dependency
useEffect(() => {
fetchData(userId);
}, []); // userId should be in the array!
// ✅ Correct
useEffect(() => {
fetchData(userId);
}, [userId]);
2. Keep Effects Focused
// ❌ Too many responsibilities
useEffect(() => {
fetchUser();
setupWebSocket();
trackAnalytics();
}, []);
// ✅ Separate concerns
useEffect(() => { fetchUser(); }, []);
useEffect(() => { setupWebSocket(); }, []);
useEffect(() => { trackAnalytics(); }, []);
Key Takeaways
- Side effects are operations outside React's render flow
useEffectruns after the component renders- The dependency array controls when the effect runs
- Always clean up subscriptions, timers, and listeners
- Don't lie about dependencies - include everything the effect uses