ReactJS Docs have a full-page introduction on useEffect
. It's a great introduction, and definitely worth your time to read it through. But it's quite a long page, and a bit hard to understand if you don't know the basics. So I would like to provide some quick walk-through before you dive deep into it.
‘useEffect
’ as its name suggests, it would apply some ‘effect’ (side effects) to your component, something like fetching data (data changes → component re-renders,) manipulating the DOM (changing document title, focusing on the input element, etc.) and doing other user actions like subscription, etc.
If you’re familiar with React class lifecycle methods, you can think of
useEffect
Hook ascomponentDidMount
,componentDidUpdate
, andcomponentWillUnmount
combined.
import React from "react";
import { useEffect, useState, memo } from "react";
const useEffectExample = () => {
const [name, setName] = useState("Adam");
const handleInputChange = (e) => {
setName(e.target.value);
};
useEffect(() => {
document.title = name;
});
return (
<div>
<input value={name} onChange={handleInputChange} />
<p>Hi {name}</p>
</div>
);
};
export default memo(useEffectExample);
This is a perfect example to show useEffect
hook being used as componentDidMount
and componentDidUpdate
combined.
useEffect
hook is called and set the document's title using the default name. useEffect
hook is called again, by setting the document's title to whatever the user enters. But sometimes we wanted to useEffect
to fetch data at the initial page render ONLY, for example, we pull the states information from API, which does not change as “name” changes. So we only need to pull it once. Let's look at the second example below:
const useEffectExample2 = () => {
const [name, setName] = useState("Adam");
const [states, setStates] = useState([]);
const handleInputChange = (e) => {
setName(e.target.value);
};
useEffect(() => {
console.log("Fetching US States Data...");
const states = fetch("StatesAPI"); //skip details here
});
return (
<div>
<input value={name} onChange={handleInputChange} />
<p>Hi {name}</p>
<Dropdown options={states} />
</div>
);
};
It works, but we see the message “Fetching US States Data…” printed in the console log not just for the initial page renders, but every time we key in the name's field, which is not necessary. To fix this, we could simply add a second parameter, an empty array, to the useEffect
hook, like this:
useEffect(() => {
console.log("Fetching US States Data...");
const states = fetch("StatesAPI"); //skip details here
}, []);
We call the second parameter as a condition array. It won't affect componetDidMount
, but for componentDidUpdate
, useEffect
will check if any member of Array changes, if so, then it will be triggered. Since we passing an empty array, nothing will changes when componentDidUpdate
is called, so then no more side effect applied.
As we mentioned above, the second parameter is an Array, and we could pass in as many conditions as needed. For example, we want to call Get Cities API based on the “state” the user selects. We could add the useEffect
hook like this:
useEffect(() => {
console.log("Fetching US States Data...");
const cities = fetch("CitiesAPI?state="+stateCode); //skip details here
}, [stateCode]);
Usually, doing the way in Usage 3 is fine: useEffect
is called in the initial page renders, which pulls the ‘cities’ data for the default ‘state’. And then when the ‘state’ changes (note that we use stateCode
as a variable name to distinguish the US State from React state,) it will pull the new ‘cities’ data for the US state selected.
But in some circumstances, we may only want to call useEffect
after componentDidUpdate
. For example, in the SSR, the initial data fetching could be done on the server-side, then we would like to skip the first data fetching on componentDidMount
. Can we do that? Well, useEffect
hook does not provide that option, but we could accomplish this by utilizing useRef
hook, like this;
// initialize a ref with current value is true
const isMounted = useRef(true)
useEffect(() => {
if(isMounted.current) { //componentDidMount
isMounted.current = false;
} else { //componentDidUpdate
console.log("Fetching US States Data...");
const cities = fetch("CitiesAPI?state="+stateCode); //skip details here
}
}, [stateCode]);
componentWillUnmount
As we mentioned at the beginning of this article, useEffect
hook is the combination of componentDidMount
, componentDidUpdate
, and componentWillUnmount
, so the last usage we would like to cover is componentWillUnmount
, which we call “Effects with Cleanup”. For side effects we looked in the previous usages, there is no need to do the cleanup, but in some cases, it is important to clean up so that we don’t introduce a memory leak! For example:
const (width, setWidth) = useState(window.innerWidth);
const hendleResize = () => {
setWidth(window.innerWidth);
}
useEffect(() => {
window.addEvenListener('resize', handleResize);
}
Since useEffect
hook is triggered after componentDidMount
and componentDidUpdate
, we would see that the same “resize” listener is added again and again. To fix this, we could add a cleanup function, which will be called before running the effect next time, like this:
const (width, setWidth) = useState(window.innerWidth);
const hendleResize = () => {
setWidth(window.innerWidth);
}
useEffect(() => {
window.addEvenListener('resize', handleResize);
return () => {
window.removeEventListener('resize');
}
}
You may notice that useEffect
clean up is different from the one using componentWillUnmount
, which is only performed when the component unmounts. This is a good thing because it prevents bugs that are common in class components due to missing update logic.
Okay, that's all. Hopefully, it helps you to understand useEffect
better.