Dec 27, 2021
React & useEffect cleanups
Before jumping into what is a useEffect
cleanup and when we need to apply it, take a look at the code below. Do you notice anything that is off?
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log("Increasing count...");
setCount((c) => c + 1);
}, 1000);
}, []);
return <h4>Count: {count}</h4>;
};
export default function App() {
const [showCounter, setShowCounter] = useState(false);
return (
<div>
<button onClick={() => setShowCounter((sc) => !sc)}>
Show/Hide Counter
</button>
{showCounter && <Counter />}
</div>
);
}
Now take a look at the demo and the console below:
So what is happening here?
- We click the button to show the Counter
- The Counter component is mounted and the interval starts
- We click the button again to hide the Counter
- The Counter component unmounts but the interval never stops, it keeps running
So despite the fact that the Counter
component unmounts after we click the button for the second time, the interval created inside the useEffect
of Counter
does not stop and it's running forever causing this error in the console:
index.js:27 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
➡️ Demo is available at: https://codesandbox.io/s/react-setinterval-memory-leak-4j60q
What is a useEffect cleanup?
In most cases when we refer to a "useEffect cleanup" we mean that we need to write some code that will guarantee that will eliminate any potential memory leaks and that our app will remain bug free.
A potential memory leak, as we saw at our counter demo before, could be that we didn't stop an interval (clear setInterval) when the component which created that interval unmounted, so now our interval keeps running forever! Or it could be that we didn't clear a subscription we created and now we've got a listener that doesn't ever get deactivated!
The situations above can very easily introduce major bugs to our app and despite the fact cleanups from useEffect are not always required, we need to acquire that as a best practice and whenever we write a useEffect
we must ask ourselves: "If the component unmounts, is unwanted code still running?" or "Do I need a cleanup function for this effect?".
How to cleanup?
Writing useEffect
cleanup functions is pretty easy and straightforward. We just return a function from our useEffect
as seen below:
useEffect(()=> {
// our effect here...
// we return the cleanup fn
return () => {
// our cleanup code here..
}
}, []);
When do cleanups run?
React performs the cleanup when the component unmounts. However, due to the fact that effects may run for many renders (and not just once), React also cleans up effects from the previous render before running the effects next time. Don't worry if it's hard to grasp this, we'll better understand it with the examples below.
Cleanup Examples
Let's fix our counter demo from before and introduce a cleanup function that will run when Counter
component unmounts and will stop the interval:
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
let interval;
interval = setInterval(() => {
console.log("Increasing count...");
setCount((c) => c + 1);
}, 1000);
// our cleanup will run when the component unmounts
return () => {
clearInterval(interval);
}
}, []);
return <h4>Count: {count}</h4>;
};
So generally speaking a subscription on a useEffect can be created and destroyed somewhat like this:
const Subscription = () => {
useEffect(() => {
createSubscription();
// our cleanup
return () => {
destroySubscription();
}
}, []);
return <div>...</div>
};
React also cleans up effects from the previous render before running the effects next time.
Based on the sentence above, let's take a look at the example below:
const FriendStatus = (props) => {
const { friendId } = props;
useEffect(() => {
// ...
subscribeToFriendStatus(friendId, handleStatusChange);
}, [friendId]);
return <div>...</div>
};
The FriendStatus
component above takes a friendId
as a prop and subscribes to the friend's status with that friendId
, which means that whenever the status of a friend changes we need to execute a function that for demo purposes we named it as handleStatusChange
.
The useEffect
will run once on mount and then whenever friendId
changes (as we have passed friendId
to our dependencies list of useEffect
).
So imagine the scenario and flow below:
- Run first effect: Mount with
friendId: 1
->subscribeToFriendStatus(1, handleStatusChange)
(friendId changes)
2. Run next effect: Update with friendId: 2
-> subscribeToFriendStatus(2, handleStatusChange)
(friendId changes)
- Run next effect: Update with
friendId: 3
->subscribeToFriendStatus(3, handleStatusChange)
The problem is that we never unsubscribe from a friendId
before subscribing to the newly updated friendId
and that means we end up having all these open subscriptions for all these friendIds!
By adding the cleanup function, as seen below, React will execute it before running effects for the next render (and of course eventually on unmount).
const FriendStatus = (props) => {
const {friendId} = props;
useEffect(() => {
// ...
subscribeToFriendStatus(friendId, handleStatusChange);
return () => {
unSubscribeToFriendStatus(friendId, handleStatusChange);
}
}, [friendId]);
return <div>...</div>
};
So the corrected flow now becomes:
- Run first effect: Mount with
friendId: 1
->subscribeToFriendStatus(1, handleStatusChange)
(friendId changes)
- Clean up previous effect: Unsubscribe from
friendId: 1
->unSubscribeToFriendStatus(1, handleStatusChange)
- Run next effect: Update with
friendId: 2
->subscribeToFriendStatus(2, handleStatusChange)
(friendId changes)
- Clean up previous effect: Unsubscribe from
friendId: 2
->unSubscribeToFriendStatus(2, handleStatusChange)
- Run next effect: Update with
friendId: 3
->subscribeToFriendStatus(3, handleStatusChange)
(component unmounts)
- Clean up previous effect: Unsubscribe from
friendId: 3
->unSubscribeToFriendStatus(3, handleStatusChange)
The End
I hope you liked this article and found it informative. Catch you later! 😀
Newsletter
Subscribe to my mailing list
Subscribe to get my latest content by email. I won't send you spam, I promise. Unsubscribe at any time.