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

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

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.

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.

Resources
Blogs | |
---|---|
The Kent C. Dodds Blog | Articles about Software Development |
People to Follow | |
Dan Abramov | Member of React's core team |
Tools | |
eslint-plugin-react | React specific linting rules for eslint |
Where to Next?
│└── JavaScript│└── ReactYOU ARE HERE│├── memo, useMemo, useCallback


