React: Let’s deep dive into deps array of useEffect

Suyeon Kang
suyeonme
Published in
4 min readApr 1, 2022
Photo by Sebastian Svenson on Unsplash

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:

  1. useEffect runs after rendering.
  2. 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 countis 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 countis 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 type
  2. Reference Type

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 useEffectwill 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:

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.

--

--