A useEffect that starts ongoing work must clean up. Timers, event listeners, and subscriptions keep running after the component leaves unless the Effect returns a cleanup function that mirrors the setup.
How cleanup works
The function returned from the Effect body is the cleanup. React runs it before the Effect runs again and one final time when the component unmounts.
The cleanup is where you stop whatever the setup started, so the two stay balanced. A cleanup that does nothing is fine when the setup does nothing that outlives the render.
useEffect(() => {
const subscription = source.subscribe(handleChange);
return () => subscription.unsubscribe();
}, []);The setup subscribes, and the cleanup unsubscribes. Keeping the two symmetric means the Effect works whether React mounts the component once or, in development, mounts it twice. A symmetrical pair is also easy to reason about because every start has exactly one stop.
Timers
A timer created with setInterval or setTimeout keeps running until it is cleared. Save the returned id and clear it in the cleanup, so a removed component stops firing.
import { useEffect, useState } from "react";
function Ticker() {
const [ticks, setTicks] = useState(0);
useEffect(() => {
const id = setInterval(() => setTicks((t) => t + 1), 1000);
return () => clearInterval(id);
}, []);
return <p>{ticks}</p>;
}The counter climbs once per second and stops when the component unmounts. Without clearInterval, the first interval would keep firing and a second one would start on the next mount. The same rule covers setTimeout, where cleanup calls clearTimeout before the pending work runs.
Event listeners
A listener added to window or document is not removed with the component. It must be removed by hand, or the page keeps running the handler on a component that no longer exists.
useEffect(() => {
const handleKey = (event) => {
if (event.key === "Escape") closeMenu();
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, []);The handler is attached while the component is mounted and detached when it leaves. Passing the same function reference to add and remove is what makes removal work. Defining the handler inside the Effect guarantees that same reference is available to both calls.
Subscriptions and observers
Anything with a subscribe, watch, or observe method usually has a matching stop method. Cancel or disconnect in the cleanup so the subscription releases its reference.
import { useEffect, useRef, useState } from "react";
function LazyImage({ src, alt }) {
const ref = useRef(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => entry.isIntersecting && setVisible(true));
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return <img ref={ref} src={visible ? src : undefined} alt={alt} />;
}The img element starts with no src, so nothing loads. Once the browser reports that the element entered the viewport, visible becomes true and the real image loads. Disconnecting in the cleanup releases the observer instead of leaving a hidden one running after the component unmounts.
Store subscriptions follow the same shape: subscribe in setup, unsubscribe in cleanup.
What to learn next
Cleanup is the second half of every Effect. If a setup line has no matching undo, that is usually the bug to look for first. For the overview of when to use the Hook at all, see React useEffect explained.
The same flag pattern also prevents race conditions when fetching.
Rune AI
Key Insights
- Return a cleanup function from useEffect to undo the setup.
- Cleanup runs before each re-sync and on unmount.
- Clear timers, remove listeners, and cancel subscriptions with the matching API.
- A symmetrical cleanup makes the Effect safe under Strict Mode remounting.
Frequently Asked Questions
When does the cleanup function run?
Do I need cleanup for every Effect?
What happens if I skip cleanup?
Conclusion
Every Effect that starts ongoing work should return a cleanup function that stops it. Pair setInterval with clearInterval, addEventListener with removeEventListener, and subscribe with unsubscribe.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.