Left Arrow

Notes

memo, useMemo, useCallback

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

React's memo, useMemo, useCallback

Tended

Status: decay

Referential equality and expensive calculations is why memo, useMemo and 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 and 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 and 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 and 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 and 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 and 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 and 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 and 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.

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.

Where to Next?

JavaScript
React
Arrow pointing downYOU ARE HERE
memo, useMemo, useCallback
A sci-fi robot taxi driver with no lower body