Left Arrow.Back
Left Arrow.Back

Notes / JavaScript / React / memo, useMemo, useCallback

memo, useMemo, useCallback

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

React's memo, useMemo, useCallback

Last Tended

Status: seed

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

import "./styles.css";
import React, { useState, Fragment } from 'react';
import { createRoot } from 'react-dom/client';

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

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

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

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <MyParent />
);
import "./styles.css";
import React, { useState, Fragment } from 'react';
import { createRoot } from 'react-dom/client';

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

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

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

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

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { useState, Fragment, memo } from 'react';
import { createRoot } from 'react-dom/client';

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

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

const MemoMyChild = memo(MyChild)

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

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <MyParent />
);
import "./styles.css";
import React, { useState, Fragment, memo } from 'react';
import { createRoot } from 'react-dom/client';

let childRenderCount = 0;
let parentRenderCount = 0;

const MyChild = () => {
  childRenderCount++

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

const MemoMyChild = memo(MyChild)

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

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

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { useState, Fragment } from 'react';
import { createRoot } from 'react-dom/client';

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] = useState(0);
  const [count2, setCount2] = useState(0);
  const increment1 = () => setCount1((value) => ++value);
  const increment2 = () => setCount2((value) => ++value);
  parentRenderCount++

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <Parent />
);
import "./styles.css";
import React, { useState, Fragment } from 'react';
import { createRoot } from 'react-dom/client';

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] = useState(0);
  const [count2, setCount2] = useState(0);
  const increment1 = () => setCount1((value) => ++value);
  const increment2 = () => setCount2((value) => ++value);
  parentRenderCount++

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

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { useState, Fragment, memo } from 'react';
import { createRoot } from 'react-dom/client';

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 = memo(Child)

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

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <Parent />
);
import "./styles.css";
import React, { useState, Fragment, memo } from 'react';
import { createRoot } from 'react-dom/client';

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 = memo(Child)

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

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

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { Fragment, memo, useState, useCallback } from 'react';
import { createRoot } from 'react-dom/client';

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 = memo(Child)

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

  parentRenderCount++

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

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <Parent />
);
import "./styles.css";
import React, { Fragment, memo, useState, useCallback } from 'react';
import { createRoot } from 'react-dom/client';

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 = memo(Child)

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

  parentRenderCount++

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

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { Fragment, useState } from 'react';
import { createRoot } from 'react-dom/client';

let componentRenderCount = 0;
let functionCallCount = 0;

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

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

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

  componentRenderCount++;

  return (
    <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>
    </Fragment>
  );
};

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <MyComponent />
);
import "./styles.css";
import React, { Fragment, useState } from 'react';
import { createRoot } from 'react-dom/client';

let componentRenderCount = 0;
let functionCallCount = 0;

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

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

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

  componentRenderCount++;

  return (
    <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>
    </Fragment>
  );
};

const container = document.getElementById('root');
const root = 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

import "./styles.css";
import React, { Fragment, useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';

let componentRenderCount = 0;
let functionCallCount = 0;

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

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

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

  componentRenderCount++;

  return (
    <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>
    </Fragment>
  );
};

const container = document.getElementById('root');
const root = createRoot(container);
root.render(
  <MyComponent />
);
import "./styles.css";
import React, { Fragment, useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';

let componentRenderCount = 0;
let functionCallCount = 0;

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

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

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

  componentRenderCount++;

  return (
    <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>
    </Fragment>
  );
};

const container = document.getElementById('root');
const root = 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.