Left Arrow

Notes

memo, useMemo, useCallback

A number of circles connected by lines. Some flashing with color.

React's memo, useMemo, useCallback

Last Tended

Status: sprout

Referential equality & expensive calculations is why memo, useMemo & useCallback are built into React. Structural types (object, array, function) declared within a React component will be a different instance every time the component renders. This leads to unnecessary re-renders. This note requires an understanding of the following:

A typed letter.

memo

Not Optimized

Imagine we had 2 React components, MyParent & MyChild. We render MyChild in MyParent. The parent also has a useState hook with a boolean value. We'll then get the parent to render a button that, when clicked, toggles the state from true to false & vice versa.

localhost:3000

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

  return (
    <output>
      child render count: {childRenderCount}
    </output>
  );
}

const MyParent = () => {
  const [isTrue, setTrue] = React.useState(false);
  const toggle = () => setTrue(state => !state)
  parentRenderCount++

  return (
    <React.Fragment>
      <button onClick={toggle}>TOGGLE</button>
      <output>
        parent render count: {parentRenderCount}
      </output>
      <MyChild />
    </React.Fragment>
  );
}

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<MyParent />);

The toggle button is there just to trigger a re-render of the parent. When the hook's state changes, the component with the hook will re-render. You will notice, however, that the child is also re-rendering (even though no changing props are being passed to it). This is because when a parent re-renders, so does its children.

Optimized

This child's re-rendering is what we call unnecessary re-renders. When the parent is re-rendering, nothing is changing in the child component. We can prevent this re-rendering by using memo. If a component is wrapped by memo & it is about to re-render, a check will be made. Memo will look at the props being passed to the component. If there is no change between the previous & the current, the component will not re-render.

localhost:3000

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

  return (
    <output>child render count: {childRenderCount}</output>
  );
}

const MemoMyChild = React.memo(MyChild)

const MyParent = () => {
  const [isTrue, setTrue] = React.useState(false);
  const toggle = () => setTrue(state => !state)
  parentRenderCount++

  return (
    <React.Fragment>
      <button onClick={toggle}>TOGGLE</button>
      <output>parent render count: {parentRenderCount}</output>
      <MemoMyChild />
    </React.Fragment>
  );
}

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<MyParent />);
A fish hook.

useCallback

Not Optimized

Below is a parent component with 2 child components. The parent keeps track of how many times the button in each child has been clicked. When a button is clicked, the state in 1 of the parent's hooks changes. This triggers a re-render on the parent, which in turn, triggers a re-render of both children.

localhost:3000

let parentRenderCount = 0;
let button1RenderCount = 0;
let button2RenderCount = 0;

const Child = ({ onClick, count, isBtn2 }) => {
  isBtn2 ? button2RenderCount++ : button1RenderCount++;

  const renderCount = isBtn2 ? button2RenderCount : button1RenderCount

  return (
    <div>
      <button 
        onClick={onClick}>
          Button {isBtn2 ? 2 :1} - clicks: {count}
      </button>
      <output>
        render count: {renderCount}
      </output>
    </div>
  );
}

const Parent = () => {
  const [count1, setCount1] = React.useState(0);
  const [count2, setCount2] = React.useState(0);
  const increment1 = () => setCount1((value) => ++value);
  const increment2 = () => setCount2((value) => ++value);
  parentRenderCount++

  return (
    <React.Fragment>
      <output>parent render count: {parentRenderCount}</output>
      <Child count={count1} onClick={increment1} />
      <Child count={count2} onClick={increment2} isBtn2 />
    </React.Fragment>
  );
}

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<Parent />);

When we click button 1, its counter increases by 1. So it makes sense that the parent & it re-render. The other button however, whose counter doesn't change, does an unnecessary re-render. We can use memo, like before, to prevent this.

localhost:3000

let parentRenderCount = 0;
let button1RenderCount = 0;
let button2RenderCount = 0;

const Child = ({ onClick, count, isBtn2 }) => {
  isBtn2 ? button2RenderCount++ : button1RenderCount++;

  const renderCount = isBtn2 ? button2RenderCount : button1RenderCount

  return (
    <div>
      <button 
        onClick={onClick}>
          Button {isBtn2 ? 2 :1} - clicks: {count}
      </button>
      <output>
        render count: {renderCount}
      </output>
    </div>
  );
}

const MemoChild = React.memo(Child)

const Parent = () => {
  const [count1, setCount1] = React.useState(0);
  const [count2, setCount2] = React.useState(0);
  const increment1 = () => setCount1((value) => ++value);
  const increment2 = () => setCount2((value) => ++value);
  parentRenderCount++

  return (
    <React.Fragment>
      <output>parent render count: {parentRenderCount}</output>
      <MemoChild count={count1} onClick={increment1} />
      <MemoChild count={count2} onClick={increment2} isBtn2 />
    </React.Fragment>
  );
}

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<Parent />);

Hmm... that didn't seem to work. When button 1 is clicked, button 2 is still re-rendering. Memo is supposed to prevent re-renders on a component if its props don't change. Ahh..., but they are. The count prop is a primitive type, its not changing. However, the onClick property is a function, a structural type. When the parent re-renders, the function is replaced by a new instance. Because of referential equality check, the old onClick doesn't equal the new onClick. Thus causing the unnecessary re-render.

Optimized

useCallback will return a memoized version of the callback that only changes if 1 of the dependencies changes. Wrapping increment1 in this hook will maintain the instance of this function when the parent re-renders (unless 'setCount1' changes). Now, when memo looks at the onClick prop, it will check if 'oldOnClick1 === newOnClick1'. This will return true, preventing a re-render.

localhost:3000

let parentRenderCount = 0;
let button1RenderCount = 0;
let button2RenderCount = 0;

const Child = ({ onClick, count, isBtn2 }) => {
    isBtn2 ? button2RenderCount++ : button1RenderCount++;

    const renderCount = isBtn2 ? button2RenderCount : button1RenderCount

    return (
        <div>
            <button 
              onClick={onClick}>
                Button {isBtn2 ? 2 :1} - clicks: {count}
            </button>
            <output>
                render count: {renderCount}
            </output>
        </div>
    );
}

const MemoChild = React.memo(Child)

const Parent = () => {
    const [count1, setCount1] = React.useState(0);
    const [count2, setCount2] = React.useState(0);
    const increment1 = React.useCallback(
        () => setCount1((c) => c + 1), [setCount1]
    );
    const increment2 = React.useCallback(
        () => setCount2((c) => c + 1), [setCount2]
    );

    parentRenderCount++

    return (
        <React.Fragment>
            <output>parent render count: {parentRenderCount}</output>
            <MemoChild count={count1} onClick={increment1} />
            <MemoChild count={count2} onClick={increment2} isBtn2 />
        </React.Fragment>
    );
}

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<Parent />);
A fish hook.

useMemo

Not Optimized

Below is a component that needs to do a computationally expensive calculation on some data before rendering it. The function that does this calculation is taking a long time to complete and is slowing down our app. Each time the input changes, the expensive function is called. This is normal. However, each time the component re-renders & the input doesn't change, the function is also called. Calling the function when the input doesn't change is unnecessary.

localhost:3000

let componentRenderCount = 0;
let functionCallCount = 0;

const expensiveFunction = (input) => {
    functionCallCount++;
    return `computated-${input}`;
}

const MyComponent = () => {
    const [isTrue, setTrue] = React.useState(false);
    const [input, setInput] = React.useState(`a`);

    const toggle = () => setTrue(state => !state)
    const value = expensiveFunction(input);

    componentRenderCount++;

    return (
        <React.Fragment>
            <output>
                component render count: {componentRenderCount}
            </output>
            <output>
                expensive function call count: {functionCallCount}
            </output>
            <output>
                value: {value}
            </output>
            <button onClick={toggle}>TOGGLE</button>
            <button onClick={() => setInput(`a`)}>a</button>
            <button onClick={() => setInput(`b`)}>b</button>
        </React.Fragment>
    );
};

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<MyComponent />);

Optimized

useMemo can prevent these unnecessary calls. useMemo returns a memoized value. It will only re-call its wrapped function if the value of a dependency changes. By wrapping the call to expensiveFunction with useMemo, we prevent it from being called unless input changes.

localhost:3000

let componentRenderCount = 0;
let functionCallCount = 0;

const expensiveFunction = (input) => {
    functionCallCount++;
    return `computated-${input}`;
}

const MyComponent = () => {
    const [isTrue, setTrue] = React.useState(false);
    const [input, setInput] = React.useState(`a`);

    const toggle = () => setTrue(state => !state)
    const value = React.useMemo(() => expensiveFunction(input), [input]);

    componentRenderCount++;

    return (
        <React.Fragment>
            <output>
                component render count: {componentRenderCount}
            </output>
            <output>
                expensive function call count: {functionCallCount}
            </output>
            <output>
                value: {value}
            </output>
            <button onClick={toggle}>TOGGLE</button>
            <button onClick={() => setInput(`a`)}>a</button>
            <button onClick={() => setInput(`b`)}>b</button>
        </React.Fragment>
    );
};

const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<MyComponent />);

In computer science, a memoised function remembers all previous inputs & outputs. It never needs to perform a calculation on the same input value twice. useMemo isn't that sophiscated. It you pass it a value, then change that value, then change the value back to the original, it will do a calculation on the original value again.

A biohazard warning sign.

Warning

You should not use memo, useMemo or useCallback unless you are noticing a performance problem. Performance optimisations aren't free. For example, when implementing useMemo, you free up CPU time but you pay by taking up more memory. If you need a reference to an object or array that doesn't require recalculation, useRef could be a better choice.

A space ship's docking bay with various equipment.

Resources

Blogs
The Kent C. Dodds BlogArticles about Software Development
People to Follow
Dan AbramovMember of React's core team
Tools
eslint-plugin-reactReact specific linting rules for eslint

Where to Next?

└── JavaScript
└── React
Arrow pointing down
YOU ARE HERE
├── memo, useMemo, useCallback
├── React Spring
└── React Three Fiber
A sci-fi robot taxi driver with no lower body