useEffect() is an escape hatch for synchronizing with external systems (APIs, DOM manipulation, third-party libraries, browser APIs).
It should not be used for orchestrating the state. React linters will warn against that.
useEffect() documentation | You Might Not Need an Effect
With dependencies - runs after the initial render and when dependencies change:
useEffect(() => {
fetchData(userId);
}, [userId]);No dependencies - runs after every render:
useEffect(() => {
console.log('Runs on every render');
});Empty array - runs once after initial mount:
useEffect(() => {
const connection = connectToServer();
}, []);Return a function from useEffect() to clean up side effects. It runs before the effect re-executes and when the component unmounts.
useEffect(() => {
const subscription = api.subscribe(id);
return () => {
subscription.unsubscribe();
};
}, [id]);Common cleanup use cases: clearing timers, canceling requests, unsubscribing from events.
In development, React runs effects twice to help find bugs. This only happens in Strict Mode.
useEffect(() => {
console.log('You should see me twice in dev mode');
return () => console.log('And me in between!');
}, []);Allows defining event handlers that always have access to the latest state and props without needing to include them in the dependency array.
const onVisit = useEffectEvent((url) => {
logAnalytics(url, shoppingCart);
});
useEffect(() => {
onVisit(url);
}, [url]);There could be more nuances to this, but generally the order of things is as follows:
- Component render logic
- React commits to DOM
useLayoutEffect()cleanupuseLayoutEffect()effect- Browser paints
useEffect()cleanupuseEffect()effect
Important notes:
useLayoutEffect()runs before the browser paints, blocking visual updates until it's done.- Cleanup functions capture state/props from the previous render (old scope).