Effect cleanup
Some effects leave something running behind them: a timer, a window event listener, a subscription. If the component leaves the page while the timer keeps ticking, you get a leak — code runs for nothing and may change the state of a component that no longer exists. To prevent this, an effect returns a cleanup function.
A timer with cleanup
Create src/components/Clock.jsx — a clock that updates every second:
// src/components/Clock.jsx
import { useEffect, useState } from 'react';
export default function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
return <p>It is {time.toLocaleTimeString()}</p>;
}Render <Clock /> in App — the time on the page changes every second. The function returned by the effect stops the timer.
When React calls cleanup
| Moment | What happens |
|---|---|
| The component leaves the page | Cleanup is called |
| A dependency changed | Cleanup of the old effect first, then the new one runs |
Development with StrictMode | Run, clean up and run again right after mounting |
Checking it on the page
Add a button to App that shows and hides the clock:
// src/App.jsx
import { useState } from 'react';
import Clock from './components/Clock.jsx';
export default function App() {
const [showClock, setShowClock] = useState(true);
return (
<>
<button onClick={() => setShowClock(!showClock)}>Clock</button>
{showClock && <Clock />}
</>
);
}When the clock is hidden, Clock is not on the page and its timer is stopped. Without cleanup the timer would keep running and errors could show up in the console.
A window event listener
Listeners added with addEventListener are removed the same way. A component showing the window width — src/components/WindowWidth.jsx:
// src/components/WindowWidth.jsx
import { useEffect, useState } from 'react';
export default function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window width: {width}px</p>;
}Resize the browser window — the number on the page updates. removeEventListener gets the same function that was passed to addEventListener.
What needs cleaning up
| What the effect starts | How to clean up |
|---|---|
setInterval, setTimeout | clearInterval, clearTimeout |
addEventListener | removeEventListener |
| A subscription to an outside source | The unsubscribe it provides |
A fetch request | A "response no longer needed" flag, see the data loading lesson |
Common mistake
Without cleanup, in development an effect with setInterval runs twice, and the counter grows by 2 per second instead of 1:
// Wrong: the timer is never stopped.
useEffect(() => {
setInterval(() => setSeconds((value) => value + 1), 1000);
}, []);If StrictMode reveals double work, it is a sign the effect is missing a cleanup function.