useEffect Hook
Side effects, data fetching, and lifecycle management
What is useEffect?
useEffect is a React Hook that lets you perform side effects in your components. Side effects are anything that affects something outside the component: fetching data, updating the DOM, timers, subscriptions, and more.
What Are Side Effects?
- • Fetching data from an API
- • Setting up subscriptions (WebSocket, events)
- • Manually changing the DOM
- • Setting timers (setTimeout, setInterval)
- • Logging
- • Storing data in localStorage
Basic Syntax
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// This runs after every render
console.log('Component rendered!');
});
return <div>Hello</div>;
}
The Dependency Array
The second argument controls when the effect runs:
// Runs after EVERY render
useEffect(() => {
console.log('Runs every time');
});
// Runs only ONCE (on mount)
useEffect(() => {
console.log('Runs once on mount');
}, []); // Empty dependency array
// Runs when dependencies change
useEffect(() => {
console.log('Count changed:', count);
}, [count]); // Runs when count changes
// Multiple dependencies
useEffect(() => {
console.log('User or page changed');
}, [userId, page]); // Runs when either changes
Dependency Array Rules
| Dependency Array | When Effect Runs |
|---|---|
undefined (no array) |
After every render |
[] (empty array) |
Once on mount only |
[value] |
On mount + when value changes |
[a, b, c] |
On mount + when any dependency changes |
Cleanup Function
Return a function to clean up when the component unmounts or before the effect runs again:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Set up the interval
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function - runs on unmount
return () => {
clearInterval(intervalId);
console.log('Timer cleaned up!');
};
}, []); // Empty array = only run once
return <p>Seconds: {seconds}</p>;
}
// Event listener example
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window width: {width}px</p>;
}
Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset state when userId changes
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <h1>{user.name}</h1>;
}
Async/Await in useEffect
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
// Can't make the effect function async directly
// So create an async function inside
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const json = await response.json();
setData(json);
} catch (error) {
console.error('Error:', error);
}
};
fetchData();
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
// With cleanup for race conditions
function SafeFetch({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
const response = await fetch(`/api/items/${id}`);
const json = await response.json();
// Only update if not cancelled
if (!cancelled) {
setData(json);
}
};
fetchData();
// Cleanup: prevent state update if component unmounts
return () => {
cancelled = true;
};
}, [id]);
return <div>{data?.name}</div>;
}
Common Use Cases
// Document title
function Page({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}
// Local storage sync
function Counter() {
const [count, setCount] = useState(() => {
return Number(localStorage.getItem('count')) || 0;
});
useEffect(() => {
localStorage.setItem('count', count);
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Focus on mount
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
Multiple Effects
function Dashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// Effect 1: Fetch user
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
// Effect 2: Fetch posts (separate concern)
useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(setPosts);
}, [userId]);
// Effect 3: Document title (separate concern)
useEffect(() => {
if (user) {
document.title = `${user.name}'s Dashboard`;
}
}, [user]);
return <div>...</div>;
}
Common Mistakes
// ✗ Missing dependency
function BadExample({ userId }) {
useEffect(() => {
fetchUser(userId); // userId used but not in deps!
}, []); // Should include userId
// ✗ Object/array as dependency (causes infinite loop)
const options = { page: 1 }; // New object every render!
useEffect(() => {
fetch('/api', options);
}, [options]); // Triggers every render!
// ✓ Fix: Memoize or use primitives
useEffect(() => {
fetch('/api', { page: 1 });
}, []); // Or use useMemo for complex objects
}
💡 ESLint Plugin
Use eslint-plugin-react-hooks to catch missing dependencies automatically!
🎯 useEffect Best Practices
- ✓ Always include all dependencies used inside the effect
- ✓ Use cleanup functions for subscriptions, timers, event listeners
- ✓ Keep effects focused—one effect per concern
- ✓ Use empty [] for one-time setup on mount
- ✓ Handle race conditions in async effects
- ✓ Consider React Query or SWR for data fetching
- ✓ Don't update state unconditionally in effects (infinite loop!)