The useEffect hook runs side effects in functional components:
import { useEffect } from 'react';
function Component() {
useEffect(() => {
console.log('Effect ran');
}, []); // Empty dependency array
return <div>Component</div>;
}
The empty [] means this effect runs once when the component mounts.
Three Dependency Patterns
No dependency array - runs on every render:
useEffect(() => {
console.log('Runs after every render');
});
Empty array [] - runs once on mount:
useEffect(() => {
console.log('Runs once when component mounts');
}, []);
Dependencies listed - runs when dependencies change:
useEffect(() => {
console.log('Runs when count changes');
}, [count]);
Component Lifecycle Mapping
For class component developers, here's the mapping:
componentDidMount:
useEffect(() => {
// Runs once after initial render
}, []);
componentDidUpdate:
useEffect(() => {
// Runs after every render (except first)
});
// Or with specific dependencies
useEffect(() => {
// Runs when prop changes
}, [prop]);
componentWillUnmount:
useEffect(() => {
return () => {
// Cleanup - runs when component unmounts
};
}, []);
Common Use Cases
Fetching data on mount:
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
}
fetchData();
}, []);
Setting up subscriptions:
useEffect(() => {
const subscription = dataSource.subscribe(handleData);
return () => {
subscription.unsubscribe();
};
}, []);
Adding event listeners:
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
The Cleanup Function
Return a function to clean up:
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(timer); // Cleanup
};
}, []);
Cleanup runs:
- When the component unmounts
- Before re-running the effect (if dependencies changed)
Dependency Array Rules
Include every value from component scope that the effect uses:
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]); // userId is a dependency
return <div>{user?.name}</div>;
}
Omitting dependencies causes stale closures—the effect uses old values.
The ESLint Rule
React's ESLint plugin warns about missing dependencies:
// ESLint warning: Include 'count' in the dependency array
useEffect(() => {
console.log(count);
}, []);
Follow these warnings. They prevent bugs.
Infinite Loop Pitfall
Object/array dependencies cause infinite loops if recreated each render:
function Component() {
const options = { refresh: true }; // New object every render
useEffect(() => {
fetchData(options);
}, [options]); // Infinite loop!
}
Fix with useMemo:
const options = useMemo(() => ({ refresh: true }), []);
useEffect(() => {
fetchData(options);
}, [options]); // Runs once
Or remove from dependencies and define inside effect:
useEffect(() => {
const options = { refresh: true };
fetchData(options);
}, []);
Function Dependencies
Functions recreated each render also cause loops:
function Component() {
function handleClick() {
console.log('clicked');
}
useEffect(() => {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [handleClick]); // New function each render
}
Fix with useCallback:
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
useEffect(() => {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [handleClick]);
Async Effects
useEffect can't be async directly. Use async function inside:
// Wrong
useEffect(async () => {
const data = await fetch('/api');
}, []);
// Correct
useEffect(() => {
async function fetchData() {
const response = await fetch('/api');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
Skipping Effects
Conditional execution inside the effect:
useEffect(() => {
if (!shouldRun) return;
// Effect logic
}, [shouldRun]);
Or conditionally define the effect:
if (shouldRun) {
useEffect(() => {
// Effect logic
});
}
// Don't do this - breaks rules of hooks
Never conditionally call hooks. Always call them at the top level.
Multiple Effects
Separate concerns into different effects:
// One effect for window resize
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Separate effect for data fetching
useEffect(() => {
fetchData();
}, [userId]);
This is clearer than one large effect.
Stale Closure Problem
Effects capture values from when they were created:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // Always uses initial count (0)
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
Fix with functional updates:
setCount(c => c + 1); // Uses current count
Or include count in dependencies (causes timer recreation).
useLayoutEffect Alternative
For effects that need to run before paint:
import { useLayoutEffect } from 'react';
useLayoutEffect(() => {
// Runs synchronously before browser paint
// Use for measuring DOM, preventing flicker
}, []);
Use useEffect for most cases. useLayoutEffect blocks visual updates.
Testing Components with useEffect
Effects run after render in tests too. Wait for effects:
import { render, waitFor } from '@testing-library/react';
test('fetches data', async () => {
render(<Component />);
await waitFor(() => {
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
});
Further Reading
React's useEffect documentation covers all behaviors and examples.
Dan Abramov's Complete Guide to useEffect is the definitive deep dive.
The React docs on synchronizing with effects explain when and why to use useEffect.
useEffect is central to modern React. Understanding the dependency array prevents common bugs and enables proper side effect management.
0 comments