SoFunction
Updated on 2025-04-07

React pit avoidance guide for useEffect Dependency Reference Type Analysis

Preface

If you are a front-end developer who has just entered the industry, you will most likely encounter a problem during the interview:

What do you think you should pay attention to when using React?

This question is intended to examine the depth of your use of React, because after immersively writing a project, you will find that unlike some frameworks that make decisions for you, React with rich "unwritten rules" is far more difficult to get along with than it seems.
There are two main types of pitfalls in React. One is to catch you off guard, but the result is not up to expectations, which seriously affects the development progress. The other is even more headache, with calm surfaces and undercurrents surging underwater.
The tentacles of the official document only reach the Demo level and do not involve all kinds of worst practices, so the next group of developers will fall into the same trap again. Hidden pit points require developers to go to the ground to clear mines themselves, and empiricism plays an important role, especially in the use of Hooks.
To avoid more mental burdens, this series of articles will introduce some common pitfalls of React, which will take you to trace the causes and explore solutions, and help beginners quickly jump through pit points.

Question raised

const Issue = function () {
  const [count, setCount] = useState(0);
  const [person, setPerson] = useState({ name: 'Alice', age: 15 });
  const [array, setArray] = useState([1, 2, 3]);
 
  useEffect(() => {
    ('Component re-rendered by count');
  }, [count]);
 
  useEffect(() => {
    ('Component re-rendered by person');
  }, [person]);
 
  useEffect(() => {
    ('Component re-rendered by array');
  }, [array]);
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(1)}>Update Count</button>
      <button onClick={() => setPerson({ name: 'Bob', age: 30 })}>Update Person</button>
      <button onClick={() => setArray([1, 2, 3, 4])}>Update Array</button>
    </div>
  );
};

In this case, three states are initialized, and the corresponding three side effect functions useEffect. The ideal state is that the useEffect is triggered only when the value of the state is updated.
Click Update Count multiple times to update State, because the updated value is still 1, so the first useEffect will not be repeated after the first execution, which is as expected. However, it is not the case when clicking Update Person and Update Array repeatedly. Although the values ​​are the same, useEffect will fire every time. When the side effects in useEffect are calculated at a large amount, performance problems will inevitably occur.

Reason traceability

To trace this reason, you can first get familiar with the source code of useEffect:

function useEffect(create, deps) {
  const fiber = get();
  const { alternate } = fiber;
 
  if (alternate !== null) {
    const oldProps = ;
    const [oldDeps, hasSameDeps] = areHookInputsEqual(deps, );
 
    if (hasSameDeps) {
      pushEffect(fiber, oldProps, deps);
      return;
    }
  }
 
  const newEffect = create();
 
  pushEffect(fiber, newEffect, deps);
}
 
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false;
  }
 
  for (let i = 0; i <  && i < ; i++) {
    if ((nextDeps[i], prevDeps[i])) {
      continue;
    }
 
    return false;
  }
 
  return true;
}

In the above code, we focus onareHookInputsEqualThe implementation of this function compares the dependencies passed in twice before and after, and determines the subsequent side effect functioncreate()Whether it will be executed. It can be clearly seen that useEffect performs a shallow comparison of dependencies, i.e. (arg1, arg2), this may be due to performance considerations. This is no problem with the original type, but for reference types (arrays, objects, functions, etc.), this means that even if the internal value remains the same, the reference itself will change, resulting in side effects of useEffect execution.

Program Exploration

1. Drinking poison to quench thirst

The seam and patching is just to wait for someone to push it down and cover it for you

The most direct idea is to change the dependency of useEffect from a reference type to a basic type:

  useEffect(() => {
    ('Component re-rendered by person');
  }, [(person)]);
 
  useEffect(() => {
    ('Component re-rendered by array');
  }, [(array)]);

It is feasible on the surface, but the actual consequences are endless (for details, why can't it be used for deep copying). Digging another pit to avoid pits is obviously not the solution we are looking forward to.
In contrast, this writing method is tolerated, but if other attributes are added to the person object, you must make sure that you still remember to update the dependencies, otherwise it will still be a cover-up problem.

useEffect(() => {
  ('Component re-rendered by person');
}, [, ]);

2. Pre-interception

The second idea:

Before you decide to take action, I have already made a decision for you - Green's formula extended axiom

We can put the problem as preceded as possible and manually add a deep comparison. How to find that the reference value has not changed, the logic of state update will not be executed, and the useEffect will not be triggered to repeat execution.

<button onClick={() => {
    const newPerson = { name: 'Bob', age: 18 };
    if (!isEqual(newPerson, person)) {
      setPerson(newPerson)}
    }
  }
>Update person</button>

But this is obviously not very elegant, and the mental burden is too heavy every time I write setState, so can the comparison logic be encapsulated?

3. Stones from other mountains

In fact, custom Hooks are designed to solve the problem-level logical reuse. Here we use the characteristics of the value bound by useRef to cross the rendering cycle to implement a custom useCompare.

const useCompare = (value, compare) => {
  const ref = useRef(null);
  if (!compare(value, )) {
     = value;
  }
  return ;
}

After the last result recorded by the ref, we have the status of two updates before and after. If we find that the values ​​are different, let the ref bind the new reference type address.

import { isEqual } from 'lodash';
 
const comparePerson = useCompare(person, isEqual);
 
useEffect(() =&gt; {
    ('Component re-rendered by comparePerson');
}, [comparePerson]);
 
// Repeat executionuseEffect(() =&gt; {
  ('Component re-rendered by person');
}, [person]);

It should be noted that lodash's isEqual function is used here to achieve deep comparison. It seems worry-free and is actually an extremely unstable choice. If the object is too large, it may not be worth the loss. You can pass in a simplified comparison function, which has relatively common key values ​​with trade-offs.
And every time I call useCompare to generate a new object separately, the logic here is also worth encapsulating.

4. Return to the essence

Stop saving the country on a curve and face the problem itself.

Having said so much, it is actually a comparison logic problem in useEffect. Based on the principle of supporting expansion but not modification, we need to support a new useEffect to support in-depth comparison. We pass the memory reference implemented by useRef into the comparison logic of useEffect:

import { useEffect, useRef } from 'react';
import isEqual from '';
 
const useDeepCompareEffect = (callback, dependencies, compare) =&gt; {
  // The default comparison function is adopted, supports customization  if (!compare) compare = isEqual;
  const memoizedDependencies = useRef([]);
  if (!compare (, dependencies)) {
     = dependencies;
  }
  useEffect(callback, );
};
 
export default useDeepCompareEffect;
 
 
function App({ data }) {
  useDeepCompareEffect(() =&gt; {
    // The code here will only be executed when there is a deep change in data    ('data has changed', data);
  }, [data]);
 
  return &lt;div&gt;Hello World&lt;/div&gt;;
}

Considering the deep contrast hidden dangers of complex objects mentioned above, I still have a personal will and add an optional parameter compare function to useDeepCompareEffect, using isEqual as a default mode. So, we finally have a one-and-one approach.

Summarize

In fact, third-party libraries such as react-use and a-hooks have implemented useDeepCompareEffect. It can also be found that custom hooks solve problems will be a highly reusable practice in the current system. Through the derivation of these methods, we can also see our idea of ​​obtaining the solution, and we hope it will be helpful to the growth of novices.

This is the article about the useEffect dependency reference type analysis of React pit avoidance guide. This is the end of this article. For more related React useEffect dependency reference type content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!