Cleanup Functions
Cleanup functions reverse work started by an effect. They prevent leaks, duplicate subscriptions, stale timers, and updates from work that no longer matters.
useEffect(() => {
const resource = startSomething();
return () => {
resource.stop();
};
}, []);When Cleanup Runs
React runs cleanup:
- before the effect runs again because dependencies changed
- when the component unmounts
- during development checks such as Strict Mode remounting
Cleanup belongs to a particular render's effect. It should clean up the work started by that same effect.
Timers
useEffect(() => {
const id = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
return () => clearInterval(id);
}, []);Without cleanup, the timer can continue after the component is gone.
Event Listeners
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);The same function reference used for addEventListener must be used for removeEventListener.
Subscriptions
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setValue(store.getSnapshot());
});
return unsubscribe;
}, [store]);Subscriptions are one of the clearest cleanup cases. If you subscribe, you must unsubscribe.
Abort Requests
Fetch requests can be aborted with AbortController.
useEffect(() => {
const controller = new AbortController();
async function load() {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data = await response.json();
setUser(data);
}
load().catch((error) => {
if (error.name !== "AbortError") {
setError(error);
}
});
return () => controller.abort();
}, [userId]);This prevents outdated requests from continuing after userId changes or the component unmounts.
Common Mistakes
Do not make the cleanup function async. React expects cleanup to run synchronously.
return async () => {
await connection.disconnect(); // avoid this pattern
};Start async shutdown if needed, but the cleanup function itself should not return a promise.
Do not forget that cleanup runs before reruns, not only on unmount.
When dependencies change, what happens before React runs the next effect setup?
Practice Challenge
Create a useWindowSize hook.
Requirements:
- read the initial window size
- subscribe to
resize - clean up the listener
- avoid crashing in non-browser environments by documenting or guarding
window
Recap
Cleanup is the other half of an effect. Any effect that starts a timer, subscription, request, listener, or connection should clearly stop or invalidate it when the synchronization is no longer current.