preface

Some time ago, BROTHER BBQ has been stuck in the mire of business, and finally finished the work before the Chinese New Year, and had time to write an article to summarize the knowledge. Take a look, the distance from the last article has been a quarter, really is the quarterly 😂. BBQ will do a good job of writing later, pretending to be a prolific blogger.

Remember that in the previous article on React Hook, we took a deep look at the source code of React Hook, including a section on the underlying principle of useEffect Hook. However, due to limited space, more useEffect application scenarios are not listed at that time. In fact, useEffect is one of the most important hooks (useState is definitely the other one) and is a fundamental core component of many custom hooks, thanks in large part to its clever and flexible usage and operation mechanism.

Today, let the barbecue brother open the useEffect for you, and summarize more details in the actual application scenarios.

The function component is really just a JavaScript function that returns the React element

As its name suggests, the React function component is actually a JavaScript function that returns JSX. JSX is just a syntactic candy. JSX essentially stands for a JS object. This object is called a React Element in React.

Function myComponent() {return (<div> <button className="red"> </div>); // Return {// type: 'div', // props: {// children: [{// type: 'button', // props: { // className: 'red', // }, // }], // }, // }; }Copy the code

(The React element returned in the example above omits many attributes that are not relevant to this article. The React element was summarized earlier by BBQ: React Fiber and Reconciliation.)

So, to be clear: the React function component is essentially a JavaScript function that returns the React element. Every time a component is rendered, it simply calls the function again.

Each rendering of a component is independent of each other

Each rendering of a component is independent of each other, and each rendering has fixed props, state, event handlers, and effects. Each function in the component (including event handlers, effects, timers, or API calls, etc.) “captures” props and state in the render in which they were defined.

Each rendering of a component has a fixed state

If the Hook useState is used in the component, useState returns the state of the render and the method to modify the state when the component renders. So state is actually a constant, equivalent to a constant, in each rendering of the component.

For example, there is a Father component:

function Father() { const [notify, setNotify] = useState(0); function onClickButton() { setNotify(notify + 1); } return (<div> <button onClick={onClickButton}>); }Copy the code

Each rendering of a component has fixed props

Each rendering of a component has fixed props. It’s actually quite understandable about this. As mentioned earlier, a function component is essentially a JS function. When the parent component props to the child component, the props is just passing the function parameters to the child component function.

For example, we now have a parent and child component:

Function Father() {const [notify, setNotify] = useState(0); function onClickButton() { setNotify(notify + 1); } return (<div> <button onClick={onClickButton}> </button> <Child counterNotify={notify} /> </div>); Function Child(props) {const {counterNotify} = props; Return (<div>counterNotify = {counterNotify}</div>); }Copy the code

When the parent component clicks on the button to change the notify value, because notify is the props of the

component, this causes a re-rendering of the Child

component. In fact, it can be approximated as re-calling the function Child(props). When the Child function is re-executed, the value of props. CounterNotify is fixed and is equivalent to a constant.

Each rendering of a component has a fixed event handler

As we discussed earlier, each rendering of a component has fixed props and state. In each render of a component (the executive-function component), the props and state of the render are identified at the beginning. Then, if an event handler (whether synchronous or asynchronous) is fired in the render, the props and state used in the event handler are the props and state used in the render.

Let’s look at a classic example: there are two buttons, one called Click Me and one called Show Alert. Let’s Click “Click Me” twice in succession, then Click “Show Alert”, then Click “Click Me” again, and then wait 3 seconds, guess what the result of “Alert” is?

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click Me
      </button>
      <button onClick={handleAlertClick}>
        Show Alert
      </button>
    </div>
  );
}
Copy the code

Each rendering of a component has a fixed effect

As mentioned earlier, during each rendering of a component (the executive-function component), props and state are identified at the beginning of the rendering. In this rendering, the props and state used in effect are the props and state used in the rendering.

Let’s start with a simple example:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
Copy the code

Here’s a more complicated example with a timer:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
Copy the code

UseEffect Specifies the useEffect dependency

We know that the call to useEffect is written as useEffect(effect, [dep]), with the first argument being the effect to execute and the second argument being the dependency. Dependencies are optional, and if they are not passed, this effect will be tagged by default every time the component is re-rendered, and then executed after the UI rendering is complete. If a dependency is passed in, React will determine if the dependency has changed every time the component is re-rendered. If any dependency has changed, React will tag this effect and wait until the component’s UI rendering is complete. If none of the dependencies have changed, the effect is tagged as “do not need to be executed” and will not be executed in the re-rendering of the component. (The details of tag can be found here)

Therefore, the useEffect dependency is used to determine whether or not effect is executed in this rendering, avoiding unnecessary repeated calls to effect.

How does React tell if a dependency has changed? Here we’re going to do a little bit of a look at the source code.

In the React. Js v17.0.1 / packages/React in the source – the reconciler/SRC/ReactFiberHooks. New. Js file, we can see that compare the rely on, The areHookInputsEqual method is called:

(/packages/react-reconciler/src/ReactFiberHooks.new.js)

The areHookInputsEqual function definition calls is:

(/ packages/react – the reconciler/SRC/ReactFiberHooks. New. Js)

As you can see from the import in the header, is is defined in shared/objectIs:

(/ packages/Shared/objectIs. Js)

Finally the truth! React uses JavaScript’s native API object.is () method to compare dependencies when they change.

The object.is () method is used to determine whether two values are the same. One thing to note is that when object. is compares data of reference types (arrays, objects, functions), it is actually comparing their references (memory addresses). (Click here to jump to the MDN documentation for usage and polyfill)

After learning about dependency contrast (object.is ()), we can clearly know how React determines whether a dependency has changed:

  • For primitive types of dependencies (numbers, strings), the value of the dependency is determined directly, and if the value has changed, the dependency has changed
  • For a dependency of a reference type (array, object, function), you determine the reference to the dependency, and if the reference has changed, you determine that the dependency has changed

Effect cleanup function

UseEffect the effect clearing function refers to the function that returns in effect and is not necessary. However, if you set some timer (setTimeout, setInterval) or event listener (addEventListener) in effect, Remember to clear them (clearTimeout, clearInterval, removeEventListener) in the cleanup function, otherwise you may accumulate many timer or event listeners repeatedly as the component is repeatedly rendered and eventually run out of memory.

Another question is: when does the effect cleanup function run? The answer is: The Effect cleanup function (which returns in Effect) in this component rendering will not be executed in this component rendering, but will be executed by default in the next component rendering, after the UI rendering (DOM update) is complete. If the dependency is [], the cleanup function is not executed until the component is destroyed.

UseEffect Execution flow

A function component using useEffect Hook runs as follows when the program is running:

Component first render:

  1. When useEffect is executed, add useEffect Hook to the Hook list, then create fiberNode updateQueue and add this effect to updateQueue. (Concepts like Hook lists, fiberNode, and updateQueue are also summarized in React Hook)
  2. Render the UI of the component;
  3. After UI rendering is completed (DOM update is completed), this effect is executed.

Component rerender:

  1. When executing useEffect, add useEffect Hook to the Hook list and judge the dependencies:
    • If no dependency is passed in (useEffect is not passed in as a second parameter), then the effect is tagged with “need to execute” (HookHasEffect).
    • If there is an incoming dependency DEPS and the dependency contrast between the current dependency and the last render changes, then the effect is tagged “need to execute”.
    • If a dependency DEPS is passed in, but the dependency does not change, no “need to execute” tag is given to the effect.
    • Suppose you have a dependency on DEPS, but an empty array is passed in[], thenDon’tGive this effect a “need to execute” tag;
  2. Render the UI of the component;
  3. DOM updated, if anyRemove function(the effect ofreturnContent), then executeThe last timeRender cleanup function; If the dependency is[], the cleanup function is not executed until the component is destroyed.
  4. Check whether this effect has a tag (HookHasEffect) that “needs to be executed”. If so, execute this effect. If not, skip this effect directly.

When the component is destroyed:

  1. The cleanup function from the last rendering of the component is executed before the component is destroyed

Supplement:

By the way, why not execute effect immediately after determining that the dependency has changed, rather than tag it first and then wait for the UI to render and decide whether to execute effect based on the tag? React does this to ensure that the DOM is updated every time an Effect is executed. This avoids the problem of effect execution blocking UI rendering (when the DOM is updated) and makes the page appear more responsive. This is one of the performance optimizations made by React.

Dependencies do not lie to React

Any state or props that are useful to a function component in effect needs to write those states or props to useEffect dependencies. React knows when to run effect and when not to run effect if the dependency contains the values used by the prime effect. If there are omissions in the dependency, you’re lying to React.

The downside of lying to React

The dependency lies to React, meaning that the value used by Effect is not included in the dependency. React does not know whether or not an effect needs to be executed, which may affect performance, interaction logic, or cause unexplained bugs.

Let’s look at some examples of dependencies lying to React:

useEffect(() => {
    document.title = 'Hello, ' + name;
});
Copy the code

In this example, Effect uses the name state of the component, but we pass in no dependencies (equivalent to lying to React: This effect has no dependencies). Thus, every time the component is re-rendered, regardless of whether the name has changed, the contents of Effect are executed, resulting in repeated and useless execution of effect, which can cause performance problems if the logic in effect is complex and time consuming.

Here’s another example:

useEffect(() => { document.title = 'Hello, ' + name; } []);Copy the code

In this example, Effect uses the name state in the component, but not in the dependency. Thus, except for the first rendering of the component, every subsequent rendering of the component, regardless of whether the name changes, does not effect, affecting the effect of the interaction (the title of the tag does not change with the name).

UseEffect (() => {document.title = 'Hello, '+ name; }, [name]);Copy the code

Let’s look at another timer example:

function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(intervalId); } []); // This is wrong! Return <h1>{count}</h1>; }Copy the code

In this example, the count state is used in effect, but the dependency is written as []. As a result, effect is executed only once between the initial rendering of the component and its destruction. The count value used in the timer callback will always be the state of the component rendering when the timer was created, i.e., 0 (as mentioned above, every component rendering has fixed state and effects). Therefore, in the callback triggered after every second, SetCount (0 + 1) is always executed. So what you see on the page is, once you go from 0 to 1, it stays 1, it doesn’t change.

One solution to the above example is to add count to the dependent array:

function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(intervalId); }, [count]); // Add count to the dependency. Return <h1>{count}</h1>; }Copy the code

After adding count to the dependency, the dependency does not lie to React, but this is not the perfect solution. The reason for this is that the timer is removed every time the component is re-rendered, and then a new timer is created:

A good way to write timers is this:

function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(n => n + 1); }, 1000); return () => clearInterval(intervalId); } []); // This is not a lie to React, because count is not explicitly referenced in this effect, so there is no need to write count to dependencies. return <h1>{count}</h1>; }Copy the code

As you can see from the figure above, there is always only one timer (the timer with id ‘AAA’) and it was cleared in time before the component was destroyed.

Use functions as dependencies

When we call another function in effect (assuming function A is called), it doesn’t matter if function A doesn’t use any of the components of this function, such as state or props. But if function A uses state or props in this function component, then it is also using state or props in effect.

Example:

const [name, setName] = useState(''); function changeName() { console.log(name); } useEffect(() => { changeName(); } []); /* equivalent to: useEffect() => {console.log(name); } []); // < effect */ -- this dependency uses name, but it doesn't use itCopy the code

Ways to deal with the “lying and cheating” in the above examples are:

Method 1: Write the entire function to useEffect, and then use the state or props used in the function as useEffect dependencies. The advantage of this is that we no longer need to consider these “indirect dependencies”. Our dependency arrays don’t lie either: we really don’t use anything in the component scope anymore in our Effect. If we modify the changeName function later to use another component state, we are more likely to realize that we are editing it in an Effect dependency. However, the disadvantage of this approach is that the function cannot be reused. If two USEeffects use this function, you can only copy and paste it into each of them.

const [name, setName] = useState(''); UseEffect () => {function changeName() {// < effect console.log(name); } changeName(); }, [name]);Copy the code

Method 2: Instead of using state directly in the function, pass the used state to the function in the form of [passing parameters].

const [name, setName] = useState(''); function changeName(argName) { console.log(argName); } useEffect(() => { changeName(name); }, [name]); // < effect uses name, so add it to dependencyCopy the code

Method three: Use functions as dependencies.

A typical misconception is that functions should not be dependencies. But if the function uses some state or props, we might forget to update the effects dependencies for those functions (” lie “to React), and our effects won’t synchronize the changes to props and state.

However, writing ordinary functions directly into dependencies creates a problem: every time a dependency has changed, the result is “dependency has changed.” The effect is the same with or without a dependency: effect is executed every time the component is re-rendered. The reason is that since the function definition is written in the outermost layer, the function is recreated whenever the component is re-rendered, causing the function index to change. As mentioned earlier, useEffect uses object.is () to compare dependencies, while functions are reference types. Object.is() actually compares reference types of data to their indexes. Therefore, the result of each comparison is “dependence has changed”.

const [name, setName] = useState(''); function changeName() { console.log(name); } useEffect(() => { changeName(); }, [changeName]); // < changeName is used as a dependency, which is equivalent to executing effect every time a component is rendered. Since a new function is created every time a component is rendered, changeName is assigned a new index.  useEffect(() => { changeName(); }); * /Copy the code

The solutions to the above problems are: Wrap the function with useCallback, using the component state or props used in the function as a dependency on useCallback, and using the return value of useCallback (the function index) as a dependency on useEffect. UseCallback essentially adds a layer of dependency checking to the function. When the useCallback dependency changes, the new functional index is returned (creating a new function), and if the useCallback dependency has not changed, the old functional index is used (using cached functions).

const [name, setName] = useState(''); Const changeName = useCallback(// <-- use useCallback encapsulation () => {console.log(name); }, [name] ); useEffect(() => { changeName(); }, [changeName]); // < changeName as a dependencyCopy the code

Use arrays or objects as dependencies

We create states not just for basic types like strings and numbers, but for arrays and objects. Because arrays and objects are reference types, when arrays and objects are used as useEffect dependencies, they are judged by whether their indexes change, rather than their specific values, just like functions. Therefore, if the value of state is a reference type, a new value is created and a new index is returned when setState() is called, resulting in useEffect changing the dependency each time.

const [user, setUser] = useState({ name: 'Tom', age: 18}); UseEffect () => {console.log('user changed ', user); }, [user]); <button onClick={() => setUser({name: 'Tom', age: 5})}> set name: Tom, age: 18 </button>);Copy the code

Another point to note is that if you rely on an attribute of an object whose value is of a primitive type (or on an element of an array whose value is of a primitive type), you are comparing values, not references.

const [obj, setObj] = useState({ a: 1 }); UseEffect (() => {console.log(' effect! '); }, [obj.a]); Return (<button onClick={() => setObj({a: <button onClick={() => setObj({a: </button> <button onClick={() => setObj({a: 2})}> Clicking this button triggers effect execution because obj. A has changed to 2 </button>);Copy the code

summary

  • The function component is really just a JavaScript function that returns the React element;
  • Each rendering of a component has fixed props, start, time handlers, and effects.
  • The underlying implementation to compare whether the useEffect dependency has changed isObject.is();
  • The useEffect dependency is used to determine whether an effect should be executed. In this way, effect can be avoided repeatedly and program execution efficiency can be improved.
  • Effect Clears the instant effect functionreturnThe content of the. The cleanup function in the component’s current render will not be executed until the next render.
  • UseEffect execution flow: when a component is rendered for the first time, an effect is executed after the UI is rendered (DOM is updated); When the component is rendering again, it will compare whether the value/reference of the dependency has changed between this rendering and the last rendering, and tag effect accordingly according to the comparison result. Then, after the UI rendering is completed, it will execute the effect clearing function of the last rendering first. Then according to the tag of this effect to decide whether to execute this effect; Before the component is destroyed, the last rendered effect cleanup function is executed;
  • The reason why the effect cleanup function from the last rendering and this effect is executed after the UI rendering is complete is to prevent the execution of the effect from blocking the UI rendering (DOM update);
  • UseEffect dependencies do not lie to React. If React does, it is not clear when effect will be executed.
  • If I take the function asuseEffectYou can get around this by writing function definitions to effects, passing state and props to functions instead of calling them directly from functions, and useCallback.
  • Treats functions, arrays, or objects asuseEffectIn contrast,Object.is()The isReference to compare.

That’s it. This article is based on my reading of the BBQ website and some blog posts, plus some practical experience. In the summary process, it is inevitable that there will be mistakes or incomplete analysis, I hope you can point out in the comments section after the barbecue, we can communicate and discuss together, looking forward to deepening the understanding of front-end knowledge through communication with you.

reference

  • React Official document Hook FAQ
  • A Complete Guide to useEffect
  • Why Do React Hooks Rely on Call Order?
  • React as a UI Runtime
  • How to fetch data with React Hooks?
  • Ojbect is MDN document
  • Baked through the React Hook
  • React Fiber and Reconciliation

Pay attention to “front-end barbecue booth” nuggets or wechat public account, the first time to get the summary and discovery of the barbecue brother.