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:
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 />);

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 />);

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.

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?


