React: Let’s deep dive into deps array of useEffect
When I was a beginner in React, I encountered unexpected bugs when dealing with useEffect
sometimes. In retrospect, I didn’t fully understand how dependency array exactly works in useEffect
.
useEffect
comes in handy compared with componentDidMount
in class component . You can run a function whenever the state is changed or fetch data when the component is mounted. But sometimes thing doesn’t go well as we expected. In most cases, we can doubt the dependency array as a suspect.
So, let’s dive into how dependency array works.
When useEffect runs?
If you don’t specify the dependency array, it will run in every render. useEffect
hook provides two types. Each hook seems similar but the running time is slightly different.
Two types of Hooks:
useEffect
runs after rendering.useLayoutEffect
runs before rendering.
useEffect(() => console.log('render'));
Or if you specify dependencies, it will only run when the dependency is changed. You might expect the below code is going to run when count
is updated. But it will run when the component is mounted as well.
const [count, setCount] = useState(0);useEffect(() => {
console.log('render');
}, [count]);
If you want to prevent running useEffect
on the first render and only to run count
is updated, you can create a custom hook using useRef
. Then you can avoid the pitfall.
const useInitialRender = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};const isInitialRender = useInitialRender();
useEffect(() => {
if(!isInitialRender) {
console.log('render');
}
}, [count])
Update conditions of dependency array
React uses Object.is()
to compare dependencies and if the values are changed, it will run a callback of useEffect
. We can approach the update conditions in two cases.
1. Primitive Types
Primitive types are consisted of:
boolean
null
undefined
number
bitInt
string
symbol
When you put primitive values into the deps array, it will run whenever the value is changed.
const useEffectTest = ({ count }) => {
useEffect(() => {
console.log('Run when primitive value is changed.');
}, [count]);
};const App = () => {
const [count, setCount] = useState(0);
const test = useEffectTest({ count }); return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Click Me</button>
</>
)
}
2. Reference Types
Reference types are consisted of:
Object
(Array and Function are object)Array
Function
If we add reference value to the deps array, we should keep in mind that useEffect
will run when the reference is changed — Reference points to the object’s location in memory. Reference is changed every time render occurs.
In other words, useEffect
runs even though the object value(e.g. user.name
) is still the same. useEffect
uses shallow equality comparison to identify whether dependencies are updated.
const useEffectTest = (user) => {
useEffect(() => {
console.log('Run when reference value is changed.');
}, [user]);
};const App = () => {
const [count, setCount] = useState(0);
const test = useEffectTest({ name: 'Suyeon', age: 27 }); return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Click Me</button>
</>
)
}
2.1 Solutions 💫
If we want to run useEffect
based on the object(reference type), we should compare dependencies with deep equal.
I will introduce 3 options:
object.property
JSON.stringify(object)
._isEqual(prev, next)
from lodashuse-deep-compare-effect
from Kent C. Dodds
1. Object.property
The most simple way is just adding object.property
as a dependency.
const useEffectTest = (user) => {
useEffect(() => {
console.log('Run when reference value is changed.');
}, [user.name]);
};
2. JSON.stringify(object)
Simply, convert the object to JSON stringified value and use it.
const useEffectTest = (user) => {
useEffect(() => {
console.log('Run when reference value is changed.');
}, [JSON.stringify(user)]);
};
3. ._isEqual(prev, next)
with useRef
You can check deep equal with utility libraries (e.g. lodash). First, capture the previous object using useRef
and just compare it with the next object. We can create a custom hook(e.g. usePrevious
) for capturing previous value.
/* usePrevious.ts */
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => ref.current = value);
return ref.current;
};import _ from 'lodash';
import usePrevious from './usePrevious';const useEffectTest = (user) => {
const prevUser = usePrevious(user); useEffect(() => {
if (!_.isEqual(prevUser, user)) {
console.log('Run when reference value is changed.');
}
}, [user]);
};
4. use-deep-compare-effect
Kent C.Dodds created a great library. You can install and use it.
import useDeepCompareEffect from 'use-deep-compare-effect';const useEffectTest = (user) => {
useDeepCompareEffect(() => {
console.log('Run when reference value is changed.');
},[user]);
};
Conclusion
useEffect
is very handy but sometimes this hook creates unexpected results. If we understand how the dependency array works, we can prevent this kind of bugs. Happy coding.