A timer can keep running after its component disappears. In React, an effect can return a cleanup function to stop work it started. React runs that function when the component unmounts; if a dependency changes, it runs the previous cleanup before running the effect again. section 6.1
Stop a stopwatch when it closes
useEffect(() => { const interval = setInterval( () => setSeconds(seconds => seconds + 1), 1000 ); return () => clearInterval(interval); }, []); The effect starts one interval when the stopwatch mounts. The empty dependency array prevents an ordinary re-render from starting another; the returned function clears the interval when the stopwatch unmounts. section 6.1.2
Match cleanup to the interaction:
- For a pauseable countdown, depend on whether it is running. Clear the old interval when that value changes or the component unmounts; start one only while running. section 6.1.5
- For a window resize listener, define the callback inside the effect, add it there, and remove that same callback in cleanup. Each component instance adds its own listener, so consider the cost of many instances. section 8.8.1
- For a slow request, cleanup can abort the request or mark the component as unmounted so a later response does not update its state. section 6.1.2
Cleanup can also stand alone. A dialog may return a function that records its dismissal or restores keyboard focus when it unmounts, without setting anything up in the effect body. This cleanup-only pattern is less common than undoing work an effect started. section 6.1.3
Listing 6.5 An interactive countdown
import { useEffect, useState } from "react";
function Countdown({ from }) {
const [seconds, setSeconds] = useState(from); #1
const [isRunning, setRunning] = useState(false); #2
useEffect(() => {
if (!isRunning) { #3
return;
}
const interval = setInterval(
() =>
setSeconds((value) => { #4
if (value <= 1) {
setRunning(false); #5
}
return value - 1; #6
}),
1000
);
return () => clearInterval(interval); #7
}, [isRunning]); #8
return (
<section>
<h2>Time left: {seconds} seconds</h2>
<button onClick={() => setSeconds(from)}> #9
Reset
</button>
<button
onClick={() => setRunning((v) => !v)} #10
disabled={seconds === 0} #10
>
{isRunning ? "Pause" : "Resume"} #11
</button>
</section>
);
}
function App() {
return <Countdown from={10} />;
}
export default App;
The interactive countdown shows why cleanup matters before a new effect, not just when a component disappears: pausing changes a dependency and clears the running interval. section 6.1.5





