Introduction
The useEffect
hook is a popular hook that combines the functionality of componentDidMount
, componentDidUpdate
, and componentWillUnmount
lifecycle methods of React
class components. The hook helps developers handle side effects in their programmes. Side effects may involve receiving data from an API, configuring event listeners, or subscribing to a WebSocket. All of these activities might influence the state of your application and may cause memory leaks if not taken care of.
The useEffect
hook comes with a cleanup function that cleans up effects to prevent memory leaks, which in turn improves the performance of the application. In this article, we will explain the cleanup function of the useEffect
hook.
What are “side-effects”?
A "side effect" is an operation that impacts something other than the function being executed.
In React, a side effect occurs when we impact something outside the scope of React in our React
components.
A side effect could be retrieving data from a remote server, reading or writing to local storage, configuring event listeners, or creating a subscription. These side effects could also occur when a button is clicked, a form is submitted, or a component is mounted and unmounted.
Why cleanup “side-effects”?
Cleaning up side effects in React
is a method of removing side effects that are no longer required. This, in turn, avoids memory leaks, which occur when your program or application attempts to perform an unnecessary operation or update a state memory location that no longer exists.
What is the useEffect
cleanup function?
The useEffect
cleanup function is a return function within the useEffect
hook.
import React, { useEffect } from "React";
useEffect(() => {
// Your effect
return () => {
// Cleanup
};
}, []);
What does useEffect
function do?
The function allows React developers to stop side effects that do not need to be executed before our component is unmounted.
For example, you have a React
component that performs a certain action in a setTimeout
. Every time the component is shown, it runs the setTimeout
function and performs the action in the timeout on that component.
Assuming You decided to navigate away from this component and into another portion of the app. The component is no longer being rendered, thus there is no need to leave the setTimeout
function running in the background. However, the setTimeout function is still running in the background.
This is where the useEffect
cleaning function comes in. In the cleanup function, we can simply add a clearTimeout
which clears the timeout when the component is unmounted (i.e., no longer rendered).
When to use the useEffect
cleanup function
There are various scenarios which will prompt the use of the useEffect
cleanup function. Thery are as follows:
Fetch requests
When initiating an API request in a component, it is important that we also account for a way to abort the request when the component is unmounted or re-rendered.
There are several methods for canceling fetch request calls; we can use the native fetch
AbortController
or Axios's
AbortController
(If you are utilizing Axios
as our API Client).
To utilize AbortController
, first, create a controller with the AbortController()
constructor. When our fetch request is initiated, we pass signal
as an option in the request's options object.
This associates the controller and signals with the fetch request, allowing us to cancel the request call at any time using the AbortController.abort()
method.
We will then add the AbortController.abort()
method to our useEffect
cleanup function to ensure that the request is aborted when the component is unmounted or re-rendered.
Examples of the useCases are displayed below:
Fetch example:
useEffect(() => {
//create the abort controller
let controller = new AbortController();
(async () => {
try {
const response = await fetch(APIEndpoint, {
// attach the controller to the request
signal: controller.signal,
});
// add the success response to a state value
} catch (e) {
// Handle the error
}
})();
//abort the request when the component umounts
return () => controller?.abort();
}, []);
Axios example:
useEffect(() => {
// create a controller
let controller = new AbortController();
(async () => {
try {
const response = await axios.get(APIEndpoint, {
// attach the controller to the request
signal: controller.signal,
});
// add the success response to a state value
} catch (e) {
// Handle the error
}
})();
//abort the request when the component umounts
return () => controller?.abort();
}, []);
Timeouts
For timeouts, you can use the setTimeout(callback, timeInMs)
timer function in the useEffect
hook, followed by the clearTimeout(timerId)
function in the cleanup function. This guarantees that the timer is cleared when the component is unmounted.
An example of this is displayed below:
useEffect(() => {
let timerId = setTimeout(() => {
// perform an action like state update
timerId = null;
}, 5000);
// clear the timer when component unmouts
return () => clearTimeout(timerId);
}, []);
Intervals
The setInterval(callback, timeInMs)
function can be declared in the useEffect
hook, and the clearInterval(intervalId)
function can be added to the cleanup function to handle intervals. By doing this, you can be sure that the timer will stop when the part is unmounted.
An example of this is displayed below:
useEffect(() => {
let intervalId = setInterval(() => {
// perform an action like state update
intervalId = null;
}, 5000);
// cleanup the interval when component unmounts
return () => clearInterval(interval);
}, []);
Event Listeners
For event listeners, you can define them in the useEffect
callback by attaching the addEventListener
function to the element, and then cleanup the listeners by attaching the removeEventListener
function to the element on the useEffect
cleanup function. This guarantees that the listener is removed from the element when the component is mounted.
A simple usage of this is displayed in the example below, which is a custom hook for handling click events that occur outside of a certain DOM
element, such as a div
.
const useOutsideClick = (callback: () => void) => {
const ref = useRef<HTMLElement>();
useEffect(() => {
const handleClick = (event: Event) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
}
};
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, [callback, ref]);
return ref;
};
Web sockets
For WebSockets, When you create a WebSocket connection in a component, you can close it when the component unmounts by including the socket.close()
method in the cleanup function.
An example of this is displayed below:
useEffect(() => {
const ws = new WebSocket(url, protocols);
// perform socket related actions
// cleanup web socket when component unmounts
return () => ws.close();
}, []);
Conclusion.
In this article, we learned about side effects and how they create memory leaks and unwanted behaviors in React
components. We also looked at how to use the useEffect
cleanup function to fix memory leaks.
Understanding how and when to repair memory leaks with the cleanup function is a significant step toward becoming a better React developer.