suemor

suemor

前端萌新
telegram
github
twitter

memo, useMemo, and useCallback in React

It has been several months since I started working with React. During this time, I have been puzzled by the issue of how to avoid unnecessary re-rendering. So today, let's talk about this topic.

Before discussing performance optimization, let's first talk about why React re-renders.

Why does React re-render?#

State changes are one of the two reasons for updates within the React tree.

import { useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  console.log('render');
  return <p>I am a very slow component tree.</p>;
};

Example of input

Clearly, whenever we enter content in the input, console.log('render') is output because the color state has changed, which indirectly indicates that it has no relation to props.

This kind of overhead is unreasonable, so we will optimize it next.

Performance Optimization#

Method 1: State Extraction#

We know that React follows a unidirectional data flow, so we only need to extract the state.

import { useState } from "react";

const App = () => {
  return (
    <div>
      <Input />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  console.log("render");
  return <p>I am a very slow component tree.</p>;
};

const Input = () => {
  let [color, setColor] = useState("red");

  return <input value={color} onChange={(e) => setColor(e.target.value)} />;
}

Method 2: memo#

React.memo is a higher-order component that turns the wrapped component into a pure component, meaning that it will only be updated if its props change.

import { memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree />
    </div>
  );
};

const ExpensiveTree = memo(() => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  return <p>I am a very slow component tree.</p>;
})

Method 3: react children#

Since App does not undergo state changes, ExpensiveTree avoids unnecessary re-rendering.

import { FC, PropsWithChildren, useState } from "react";

const App = () => {
  return (
    <ColorWrapper>
      <ExpensiveTree />
    </ColorWrapper>
  );
};

const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  return <p>I am a very slow component tree.</p>;
};

Using useMemo and useCallback#

useMemo#

useMemo is similar to Computed in Vue. It only recalculates the value when the dependencies change.

This way, when the input changes, dirtyWork will not be repeatedly executed.

import { useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const [number,setNumber] = useState(0)
  
  const dirtyWork = useMemo(() => {
    console.log('Doing a lot of work');
    return number
  },[number])
  
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <h1>{dirtyWork}</h1>
    </div>
  );
};

Additionally, we can also modify the example from the previous section using useMemo.

import { memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {useMemo(
        () => (
          <ExpensiveTree />
        ),
        []
      )}
    </div>
  );
};

const ExpensiveTree = () => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  return <p>I am a very slow component tree.</p>;
};

useCallback#

Let's look at the following example first.

import { FC, memo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = ()=> {
    console.log('hahaha');
  }
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  console.log('render'); // Still gets updated
  return <p>I am a very slow component tree.</p>;
})

We can see that even though ExpensiveTree is wrapped with memo, it still gets updated when content is entered in the input. To solve this, we just need to wrap the parent component's fn function with useCallback.

Therefore, useCallback is generally used when passing functions to child components. Let's rewrite the above example using useCallback:

import { FC, memo, useCallback, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useCallback(()=> {
    console.log('hahaha');
  },[])
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn}/>
    </div>
  );
};

const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  console.log('render');
  return <p>I am a very slow component tree.</p>;
})

You may notice that useCallback is actually syntactic sugar for useMemo. The above example can also be rewritten using useMemo:

import { FC, memo, useMemo, useState } from "react";

const App = () => {
  let [color, setColor] = useState("red");
  const fn = useMemo(() => {
    return () => console.log("hahaha");
  }, []);
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <ExpensiveTree fn={fn} />
    </div>
  );
};

const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Delay
  }
  console.log("render");
  return <p>I am a very slow component tree.</p>;
});

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.