
Cleanup Function Timing in useEffect
Effect cleanup runs before the next effect execution and before unmount. Getting the timing right prevents memory leaks, stale subscriptions, and double-execution bugs.
Cleanup Runs Before Next Effect
useEffect(() => {
const sub = subscribe(channel);
console.log('subscribed');
return () => {
sub.unsubscribe();
console.log('unsubscribed');
};
}, [channel]); When channel changes:
- Cleanup of previous effect runs:
unsubscribed - New effect runs:
subscribed
React guarantees cleanup runs before the next effect for the same hook. The new effect never runs while the old subscription is still active.
Cleanup on Unmount
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer); // runs when component unmounts
}, []); Without the cleanup, the interval keeps firing after unmount, calling tick() (which likely calls setState) on a dead component.
StrictMode Double-Run
React StrictMode (dev only) mounts → unmounts → remounts every component, exercising the cleanup path:
useEffect(() => {
console.log('mount');
return () => console.log('cleanup');
}, []);
// Dev output:
// mount
// cleanup ← StrictMode simulated unmount
// mount ← actual second mount If the second mount breaks something, your cleanup is incomplete.
Async Effects
Effects can’t return Promises — only a cleanup function or nothing:
// Wrong: async effects can't return cleanup
useEffect(async () => {
const data = await fetch('/api');
setData(data);
}, []);
// Right: async inside, sync cleanup
useEffect(() => {
let cancelled = false;
async function load() {
const data = await fetchData();
if (!cancelled) setData(data); // guard against stale update
}
load();
return () => { cancelled = true; };
}, []); Or use AbortController for fetch:
useEffect(() => {
const controller = new AbortController();
fetch('/api', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => { if (err.name !== 'AbortError') setError(err); });
return () => controller.abort();
}, []); Cleanup Reference Table
| Resource | Cleanup |
|---|---|
setInterval | clearInterval |
setTimeout | clearTimeout |
addEventListener | removeEventListener |
| WebSocket | ws.close() |
| Observable | sub.unsubscribe() |
| Fetch | AbortController.abort() |
| MutationObserver | observer.disconnect() |
| IntersectionObserver | observer.disconnect() |









