Think twice before use useMemo() or memo()

·

3 min read

There are so many articles or tutorial that teach about React performance optimization using memo() and useMemo().

But in some case, you don't need to use memo() or useMemo() for that. In this article, I want to share two different techniques that very basic but improve rendering performance.

Let's go

Problem with a very slow component.

Here I have a very slow component

import { useState } from 'react'

export default function App() {
    const [color, setColor] = useState('red')
    return (
        <div>
            <input value={color} onChange={(e) => setColor(e.target.value)} />
            <p style={{ color }}>Hi Edward</p>
            <ExpensiveTree />
        </div>
    )
}

function ExpensiveTree() {
    let now = performance.now();
    while (performance.now() - now < 100) {
        // Do nothing for 100ms    
    }
    return <p> This is a slow component </p>
}

There problem is that whenever color change inside App, we will re-render <ExpensiveTree /> which is a very slow component.

You can use memo() to put around ExpensiveTree but I will show another ways.

Solution 1: Move State Down

If you look the code, you'll notice that not all App component defend about the color variable

export default function App() {
  let [color, setColor] = useState('red'); 
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree /> -> This part doesn't defend on color variable.
    </div>
  );
}

So we can extract that part into a Form component and move state down into it:

export default function App() {
    return (
        <>
            <Form />
            <ExpensiveTree />
        </>
    )
}

const Form = () => {
  let [color, setColor] = useState('red');
  return (
     <div>
       <input value={color} onChange={(e) => setColor(e.target.value)} />
       <p style={{ color }}>Hello, world!</p>
     </div>
   );
}

If the color changes, only the Form re-renders. Done.

Solution 2: Lift Content Up.

The above solution doesn't work if the piece of state is used somewhere above the ExpensiveTree. For example, let's say we put the color on the parent <div>

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

Now it seems like we can't just "extract" the parts that don't use color into another component, since that would include the parent <div>, which would then include <ExpensiveTree />. Can't avoid memo this time right ?

But there still have solution for this:

export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}

function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

Do you get it ?

We split the App component in two. The parts that depend on the color, together with the color state variable itself, have moved into ColorPicker.

The part that don't care about the color stayed in the App component and are passed to ColorPicker as JSX content, also known as the children prop.

When the color changes, ColorPicker re-renders, But it still has the same children props it got from the App last time, so React doesn't visit that subtree.

And as a result, <ExpensiveTree /> doesn't re-render.

Two solution above are very simple to use, so think twice before apply memo() or useMemo().

Happy Coding.