preface

More and more people are building pages using React Hooks + function components. Function components are concise and elegant, which are complemented by Hooks that allow them to have internal state and side effects (life cycles).

However, the use of functional components also brings some additional problems. Because the function is re-executed when the state is updated inside a functional component, two performance problems can occur:

  1. Causes unnecessary re-rendering of child components
  2. Causes some code (computation) to repeat within the component

Fortunately, the React team is aware of the performance issues that can occur with function components, and provides the React. Memo, useMemo, and useCallback apis to help developers optimize their React code. Before using them for optimization, I think we need to be clear about what we’re using them for:

  1. Reduce unnecessary re-rendering of components
  2. Reduce double calculation within components

1 Use React. Memo to avoid duplicate renderings of components

Before we talk about how react. memo works, let’s consider the following question: when do we need to rerender a component?

In general, there are three situations in which a component needs to be rerendered:

  1. Components,stateWhen change occurs
  2. Used internally by the componentcontextWhen change occurs
  3. Externally passed by the componentpropsWhen change occurs

For now, let’s just focus on point 3: rerender when the props changes, which is an ideal situation. Because if a parent component re-renders, the child component will re-render even if nothing changes to its props, we call this a non-essential re-render. This is where the react. memo comes in handy.

First, React. Memo is a higher-order component.

The Higher Order Component is like a factory: throw a Component in and return a processed Component.

The react. memo wrapped component does a shallow comparison of the old and new props before rendering:

  • If the old and newpropsShallow comparisons are equal, then no rerendering is done (using cached components).
  • If the old and newpropsShallow comparisons are not equal, then re-render (re-rendered components).

While the above explanation may seem abstract, let’s look at a concrete example:

import React, { useState } from 'react';

const Child = () = > {
  console.log('Child rendered ');
  return <div>Child</div>;
};

const MemoChild = React.memo(() = > {
  console.log('MemoChild rendered ');
  return <div>MemoChild</div>;
});

function App() {
  const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () = >{ setIsUpdate(! isUpdate);console.log('Click the button');
  };
  return (
    <div className="App">
      <Child />
      <MemoChild />
      <button onClick={onClick}>Refresh the App</button>
    </div>
  );
}

export default App;
Copy the code

In the example above: Child is a common component and MemoChild is a component wrapped in React.memo.

When I click the button, I call setIsUpdate to trigger re-render of the App component.

The console results are as follows:

As shown above: Both Child and MemoChild are rendered for the first time, the console prints Child rendered and MemoChild rendered.

When I click on the button to re-render, Child will still re-render, and MemoChild will determine the old and new props, because MemoChild has no props, that is, the old and new props are equal. MemoChild uses the previous render results (caching), avoiding rerendering.

Rerendering a component in React will cause all its children to be rerendered without any optimization. The react. Memo wrapper prevents unnecessary re-rendering of the component when its parent is re-rendered.

React executes function components and generates or updates the virtual DOM tree (Fiber tree). There is also a DOM Diff process before rendering the real DOM (the Commit phase), comparing the differences between the virtual DOM and rendering the changed DOM. Otherwise, React performance would explode if the real DOM was re-rendered every time the state was changed (laughs).

2 Use useMemo to avoid double counting

const memolized = useMemo(fn,deps)

The React useMemo takes fn and dependency array deps as arguments. The useMemo executes fn and returns a memolized value. It recalcates memolized only when a dependency changes. This optimization helps avoid costly calculations every time you render. For details, see the following example:

import React, { useMemo, useState } from 'react';

function App() {
  const [list] = useState([1.2.3.4]);
  const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () = >{ setIsUpdate(! isUpdate);console.log('Click the button');
  };

  // Add a list
  console.log('Ordinary computing');
  const sum = list.reduce((previous, current) = > previous + current);

  // The cache computes the sum of lists
  const memoSum = useMemo(() = > {
    console.log('useMemo computing');
    return list.reduce((previous, current) = > previous + current);
  }, [list]);

  return (
    <div className="App">
      <div> sum:{sum}</div>
      <div> memoSum:{memoSum}</div>
      <button onClick={onClick}>Rerender App</button>
    </div>
  );
}

export default App;
Copy the code

In the above example, Sum is a common computed value obtained from list, memoSum is a momelized value (cached value) obtained from useMemo, and the dependency is list.

As shown in log in the console above:

  1. For the first rendering, both Sum and memoSum are computed based on the values of list;

  2. When [Re-Render App] button is clicked, although the list is not changed, the sum value is recalculated, while memoSum is not recalculated, using memolized from the previous calculation.

  3. When the add a number to List button is clicked, the List value changes and the values of sum and memoSum are recalculated.

Summary: Within function components, some derived values based on State and some complex calculations can be optimized for performance with useMemo.

Use useCallback to avoid repeated rendering of child components

const memolizedCallback = useCallback(fn, deps);

React useCallback takes the callback fn and the dependency array deps as arguments and returns a cached callback memolizedCallback (essentially a reference). It regenerates the memolizedCallback only when a dependency changes. When you pass memolizedCallback as a parameter to a child component (wrapped in React. Memo), it avoids unnecessary child rerendering.

Similarities and differences between useCallback and useMemo

UseCallback and useMemo both cache the corresponding value and update the cache only when the dependency changes. The difference is:

  • useMemoThe callback function passed in is executed and returnsThe result of the function execution
  • useCallbackInstead of executing the incoming callback function, returnsFunction reference

Incorrect use of useCallback

Many beginners (myself included) have the misconception that all functions declared inside a function component are wrapped in useCallback, thinking that this will optimize performance by avoiding repeated generation of functions, but it is not:

  1. First of all, function creation is very fast inside JS, and this performance issue is not an issue.
  2. Secondly, useuseCallbackThere is an additional performance loss because of the additionaldepsVary judgment.
  3. Each function usesuseCallbackWrap a layer, not only appear bloated, but also need to writedepsArray, additional mental burden.

UseCallback Indicates the correct usage scenario

  1. Function Functions defined internally by the component need to be dependent on other Hooks.
  2. Functions Functions defined within a component need to be passed to its children, which are wrapped by react.Memo.

Scenario 1: useCallback is mainly used to avoid reexecuting other Hooks due to function reference changes when the component is re-rendered, or even causing the component to render indefinitely:

import React, { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(1);
  const add = () = > {
    setCount((count) = > count + 1);
  };
  useEffect(() = > {
    add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
Copy the code

In the example above, useEffect will execute the add function to trigger the re-rendering of the component, which will re-generate the reference to Add, which will trigger the re-rendering of the component. , thus causing an infinite loop:

UseEffect execution -> add execution -> setCount execution -> App re-render -> add re-build -> useEffect execution -> add execution ->…

To avoid the above situation, we can solve the problem of infinite loop by covering the add function with a useCallback layer to avoid changing function references:

import React, { useCallback, useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(1);
  // Wrapping add with useCallback only generates the function reference the first time the component renders, and then reuses the reference the first time the component re-renders.
  const add = useCallback(() = > {
    setCount((count) = > count + 1); } []); useEffect(() = > {
    add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
Copy the code

Scenario 2: useCallback is used to avoid unnecessary re-rendering of child components due to callback function reference changes. (This subcomponent has two premises: first, it receives callback functions as props, and second, it is wrapped in react.Memo.)

const Child = React.memo(({ onClick }) = > {
  console.log(`Button render`);
  return (
    <div>
      <button onClick={onClick}>child button</button>
    </div>
  );
});

function App() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  // Case 1: unwrapped useCallback
  const onClick = () = > {
    setCountA(countA + 1);
  };
  // Case 2: Package useCallback
  const onClick = useCallback(() = > {
    setCountA(countA + 1); } []);return (
    <div>
      <div>countA:{countA}</div>
      <div>countB:{countB}</div>
      <Child onClick={onClick1} />
      <button onClick={()= > setCountB(countB + 1)}>App button</button>
    </div>
  );
}
Copy the code

In the example above, the Child component is wrapped in react. Memo and receives the onClick function as the props argument.

  • Case 1:onClickNo packageuseCallbackWhen clickapp buttonTrigger to re-render,onClick Regenerate the function reference, resulting inChildChild components are re-rendered.
  • Situation 2:onClickThe parceluseCallbackWhen clickapp buttonTrigger to re-render,onClick No new references are generatedTo avoid theChildChild components are re-rendered.

4 summarizes

The react. Memo, useMemo, and useCallback apis are used to avoid performance problems that may occur when using function components.

  • throughReact.memoWrap components to avoid unnecessary re-rendering of components.
  • throughuseMemoTo avoid the double calculation caused by component updates.
  • throughuseCallbackTo avoid repeated component rendering due to function reference changes.

Refer to the article

  • React
  • Segmentfault The useCallback has always been used incorrectly