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 AbortController for race conditions (recommended)
function SafeFetch({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(`/api/items/${id}`, {
signal: controller.signal,
});
const json = await response.json();
setData(json);
} catch (err) {
// AbortError is expected when cleanup runs
if (err.name !== 'AbortError') {
console.error('Fetch error:', err);
}
}
};
fetchData();
// Cleanup: abort the fetch on unmount or id change
return () => controller.abort();
}, [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!
⚠️ Strict Mode Double-Firing
In development with React.StrictMode (default in Vite/Next.js), React intentionally runs every effect twice (mount → unmount → mount) to help you find missing cleanup functions. This is normal and does NOT happen in production.
If your effect runs twice and causes problems, it means your cleanup is incomplete. Fix the cleanup rather than removing StrictMode.
📡 Data Fetching Recommendation
The React team recommends using purpose-built data fetching solutions instead of raw useEffect + fetch for production apps. Libraries like TanStack Query (React Query), SWR, or framework-level data fetching (Next.js Server Components, Remix loaders) handle caching, deduplication, background refetching, and error handling 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!)