Why Reactâs Cleanup Function Matters More Than You Think
The cleanup function in Reactâs useEffect isnât just a patternâitâs essential for managing side effects correctly. This post explains what cleanup really means and how it prevents issues like memory leaks, duplicate intervals, and lingering event listeners in real-world scenarios.
One part of useEffect that often gets overlooked is the function it returns.
At a glance, itâs easy to ignore â especially when examples only show simple logs. But that returned function turns out to be the key to handling side effects properly in real applications.
What âCleanupâ Actually Means
A cleanup function is not some special React concept.
It simply means:
Undo whatever the previous effect did
If your effect starts something, the cleanup should stop it.
Thatâs the entire idea.
Why Cleanup Is Necessary
React re-runs effects when dependencies change.
Before running the new effect, React gives you a chance to clean up the previous one.
If you donât, things start stacking up.
Example 1: setInterval
This is the classic example.
Without cleanup
useEffect(() => {
setInterval(() => {
console.log("tick")
}, 1000)
}, [])At first, this looks fine.
But if the component re-renders or mounts multiple times:
- Multiple intervals will run
- They will never stop
- Youâll get duplicate logs and memory leaks
With cleanup
useEffect(() => {
const id = setInterval(() => {
console.log("tick")
}, 1000)
return () => {
clearInterval(id)
}
}, [])Now:
- Effect â starts interval
- Cleanup â stops interval
Nothing leaks, nothing duplicates.
Example 2: Event Listener
Another common case is attaching events.
Without cleanup
useEffect(() => {
window.addEventListener("resize", () => {
console.log("resized")
})
}, [])Problem:
- Every render adds a new listener
- Old ones are never removed
- Performance degrades over time
With cleanup
useEffect(() => {
const handler = () => console.log("resized")
window.addEventListener("resize", handler)
return () => {
window.removeEventListener("resize", handler)
}
}, [])Now:
- Effect â attach listener
- Cleanup â remove listener
Example 3: Dynamic Effects (Dependency Change)
This is where cleanup becomes even more important.
useEffect(() => {
const id = setInterval(() => {
console.log(value)
}, 1000)
return () => clearInterval(id)
}, [value])When value changes:
- Cleanup runs â clears old interval
- New effect runs â starts new interval
If you skip cleanup, youâll end up with multiple intervals running at once, each using different values.
What React Is Actually Doing
React doesnât know what your effect does.
So it follows a simple rule:
- Before running a new effect â clean up the previous one
- When component unmounts â clean up the last one
This keeps your app predictable.
A Better Way to Think About It
Instead of thinking in terms of lifecycle or timing, think in terms of behavior:
- Effect = setup
- Cleanup = teardown
If your effect creates something external:
- a timer
- a listener
- a connection
Then your cleanup should remove it.
Final Takeaway
The most important realization is this:
Cleanup is not optional â itâs what keeps your side effects under control.
Once you start thinking of useEffect as âsetup + teardown,â the cleanup function stops feeling confusing and starts feeling necessary.