- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Effect Cleanup
Effects and Refs
Effect Cleanup
Whatever an effect starts, its cleanup must stop. React runs cleanup before the next effect and again when the component unmounts.
Returning a cleanup function
If an effect returns a function, React calls it before running the effect again and when the component leaves the tree. Anything ongoing - a timer, a listener, a subscription - needs one.
A timer that stops
jsx
function Ticker() {
const [seconds, setSeconds] = React.useState(0)
React.useEffect(() => {
const id = setInterval(() => setSeconds((s) => s + 1), 1000)
// Without this, the interval keeps firing after unmount.
return () => clearInterval(id)
}, [])
return <p>Running for {seconds}s</p>
}
function Demo() {
const [show, setShow] = React.useState(true)
return (
<div>
<button onClick={() => setShow((s) => !s)}>{show ? "Unmount" : "Mount"} the ticker</button>
{show && <Ticker />}
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Demo />)Without the clearInterval, unmounting would leave the timer running and calling a setter on a component that no longer exists - a leak that survives until the page is closed.
Event listeners
Add and remove in pairs
jsx
function WindowWidth() {
const [width, setWidth] = React.useState(window.innerWidth)
React.useEffect(() => {
const onResize = () => setWidth(window.innerWidth)
window.addEventListener("resize", onResize)
return () => window.removeEventListener("resize", onResize)
}, [])
return <p>Preview width: {width}px — resize the panel to see it change.</p>
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<WindowWidth />)The same function reference must be passed to both addEventListener and removeEventListener, which is why it is named rather than inlined twice.
Ignoring a stale response
You cannot un-send a network request, but you can decide to ignore its answer. Without this, a slow first request can land after a fast second one and overwrite newer data with older.
The ignore flag
jsx
function Loader({ id }) {
const [text, setText] = React.useState("idle")
React.useEffect(() => {
let ignore = false
setText("loading…")
// Stand-in for a fetch: later ids resolve faster.
const delay = id === 1 ? 1200 : 200
const timer = setTimeout(() => {
if (!ignore) setText(`data for id ${id}`)
}, delay)
return () => {
ignore = true
clearTimeout(timer)
}
}, [id])
return <p>{text}</p>
}
function Demo() {
const [id, setId] = React.useState(1)
return (
<div>
<button onClick={() => setId(id === 1 ? 2 : 1)}>Switch id (now {id})</button>
<Loader id={id} />
<p style={{ color: "#64748b" }}>
id 1 is slow. Switch quickly to 2 — the stale result is discarded.
</p>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Demo />)The double run in development
In development React mounts, unmounts and remounts each component once, so every effect runs twice and its cleanup runs in between. This is deliberate: an effect that breaks under it has a missing or incorrect cleanup.
It does not happen in production. Treat it as a test rather than a bug - if a doubled effect causes a problem, the cleanup is what needs fixing.
- Return a function from the effect to clean up.
- Cleanup runs before the next effect and on unmount.
- Pair every subscribe with an unsubscribe.
- Use an ignore flag for requests you cannot cancel.
