A Closer Look at React Memoize Hooks: useRef, useCallback, and useMemo

Christian Nwamba
April 15, 2019
The best way to understand what memoize hooks are is to expose ourselves to the problem space which these hooks solve. Starter hooks examples don’t expose these problems. They make hooks look approachable and straightforward— which makes complete sense.
The real problem starts to unravel when you use hooks more often. Spend a few more hours if you are a hooks beginner, and you will find yourself in weird pitfalls that you would not have noticed on your first day with React.
Why Memoize Hooks
A memoize hook remembers.
That is the simplest way to think about these hooks. The actual tricky questions are:
- Why do they need to remember?
- What do they remember?
- When do they remember?
I am going to attempt to teach you memoize hooks by trying as much as I can to answer these questions.
1. Why Do Hooks (React) Need to Remember?
To understand why hooks need to remember (memoize), we need to understand the motivation behind memoization in React. Memoization in programming is a computation strategy where functions remember the output of their previous execution, then uses it as a factor for the next computation. Usually, the function would try to run for each computation in a range of data but instead, it runs only once for the next range then factors the previous result.
This said, if you have written React for a few months, you must have encountered shouldComponentUpdate
or PureComponent
. These components help React to skip re-rendering (DOM computation and reconciliation) when the state update does not directly affect the UI.
This makes sense because if a component App
had a structure like this:
<App>
<Form>
<Input></Input>
<Button></Button>
</Form>
<List>
<ListItem></ListItem>
</List>
</App>
Assuming that Input
triggers a state update in App
, the entire component tree will re-render. List
will re-render, ListItem
will re-render — for goodness sake, we just probably checked a checkbox in Form
.
Ok maybe I am being too dramatic, this structure is too small to make us worry about performance. But what if we had 5 -10 or more deeply nested components? We might end up having to pay some price.
Here is a counter example to give you a hands-on experience:
Open the demo in a preview, then open the React Dev Tool tab. Click the settings icon in the tab and enable “Highlight Updates”:

When you start incrementing or decrementing by clicking each of those buttons, notice that every single component is flashing with changes (re-render):
Pay closer attention to the buttons. See how they have the blue borders showing that they are also re-rendering. When properly considered, the only component that deserves to re-render is the Counter
component since it carries the actual visual changes. The rest Button
components should not be repainted.
As mentioned earlier, React uses shouldComponentUpdate
(sCU) and PureComponent
to control updates like these. This is how we can refactor the Button
component to not update every single time the count changes:
class Button extends React.Component {
shouldComponentUpdate(nextProps, nextState) { if (this.props.padding !== nextProps.padding) { return true; } return false; }
render() {
const { children, onClick, padding } = this.props;
return (
<button onClick={onClick} style={{ padding }}>
{children}
</button>
);
}
}
I am telling Button
through shouldComponentUpdate
that “hey no matter what happens, only re-render when it’s only padding
that changed. Which means if count
changes in App
, Button
won’t change:
You can see for yourself by testing with the Codesandbox:
If you’re only concerned about shallow comparisons like this.props.padding !== nextProps.padding
, (which is what is the case most times), then you can make your class extend PureComponent
instead of Component
:
class Button extends React.PureComponent { render() {
const { children, onClick, padding } = this.props;
return (
<button onClick={onClick} style={{ padding }}>
{children}
</button>
);
}
}
This is supposed to work as stated in the docs but for some reason, it does not work for the above example. If you increment or decrement, you will still see both buttons happily flashing colors. What did I do wrong?
The onClick
prop passed down to Button
is the hidden bug. The App
component sends down a brand new version of that onClick
function every time re-rendering happens. How do we cache this function? Well, remember the good old this
binding in the constructor? It does the trick!
class App extends React.Component {
constructor(props) {
super(props);
this.increment = this.increment.bind(this); }
state = {
count: 0
};
increment() { this.setState({ count: this.state.count + 1 }); } render() {
return (
<div class="App">
<Counter count={this.state.count} />
<Button onClick={this.increment} padding={8}> Increment
</Button>
<Button
onClick={() => {
this.setState({ count: this.state.count - 1 });
}}
padding={8}
>
Decrement
</Button>
</div>
);
}
}
Extracting the arrow function into an action instance function, then create a contextual this
binding in the constructor
. This will cache/memoize the function for subsequent renders.
Here is a live code to get your hands dirty:
Two things we can observe:
- We have seen how to control re-rendering using
sCU
andPureComponent
. - We also saw how to memoize callbacks so they don’t re-render.
These two observations are only possible in Class components. We can’t achieve this in functional components because:
- Functional components cannot have instance methods so no
sCU
- Functional components cannot extend other classes so no
PureComponent
- Functional components do not have contractors and cannot have instance methods so we cannot cache/memoize callbacks
Memoize Hooks help Functional Components solve these challenges.
This answers question 1: Why do they need to remember? Why do we need to memoize?
Here is a compelling table I made to show you why memoization with React Hooks is preferred over other components
Maybe for optimized update render in class components if you remember to use shouldComponentUpdate
2. What do they remember?
Question one already gave an implicit answer to this one. Memoize hooks need to remember data or functions that:
- Might cause re-render when re-rendering is not needed
- Preserve or cache data or function so new copies are not created.
3. When do they remember?
This one is straight-forward — they remember during re-render.
Relationship between Data Hooks and Memoize Hooks
Last week I wrote about Data Hooks. Data Hooks are hooks that store data. Storing is different from memoizing/caching. You store data that a given portion UI directly relies on for visual changes and memoize/cache data that a given portion UI don’t directly rely on for visual changes.
There’s a blurry line though. useRef
is a hook that can play both roles depending on how it’s used. In the linked article, you will see how useRef
is used as a data hook. In this article you will see how to use useRef
as a memoize hook.
useState
and useRef
are data hooks. useRef
, useCallback
and useMemo
are memoize hooks. Here is a Venn diagram to help you visualize the relationship better:
I wrote about data hooks last week where I threw a lot of light on useState
and useRef
. At the end of this post, you will understand how useRef
is also a memoize hook.
Memoizing with useMemo
useMemo
is the actual memoize hook by design. The rest are memoize hooks by chance. useMemo
memoizes by taking a function that needs to be memoized and an array of values that when changed, would invalidate the memoization.
In the example below, I refactored all the components we had initially, to functional and hook components:
And we are back to our buttons re-rendering for no good reason:
We can wrap the button(s) in useMemo
and pass down values we want to be responsible for changes. In this case, we don’t even want the buttons to re-render for any reason:
function App() {
const [count, setCount] = React.useState(0);
return (
<div class="App">
<Counter count={count} />
{React.useMemo( () => ( <Button onClick={() => { setCount(count + 1); }} padding={8} > Increment </Button> ), [] )} <Button
onClick={() => {
setCount(count - 1);
}}
padding={8}
>
Decrement
</Button>
</div>
);
}
I wrapped only the increment Button
so the flashing can prove it’s only increment that does not get re-rendered:
Oh, snap! Though increment is not re-rendered, it only works once.
Well, I did this intentionally to show you some pitfalls you can have with memoizing. I am memoizing the function that returns the component which means it will cache the first output of that function and keeps giving you the same result. You can see the effect of this from the fact that count
is trapped and only shows 1.
If we were just logging a static value or performing a one off operation that does not rely on the next state of our application, the this would be a great choice. For example:
React.useMemo(
() => (
<Button
onClick={() => {
console.log('I got clicked');
}}
padding={8}
>
Increment
</Button>
),
[]
)
The sentence “I got clicked” will keep getting logged which means the function is always executed. The only issue is that the function is cached and unaware of the next external state of your app.
You can solve this by passing count
to the array but that will start re-rendering the increment
button which disputes the point of trying to memoize.
React has a different method for specifically replacing PureComponent
and that’s memo
. If we wrap Button
with React.memo
(not React.useMemo
) like this:
const Button = React.memo(({ children, onClick, padding }) => {
return (
<button onClick={onClick} style={{ padding }}>
{children}
</button>
);
});
In as much as this like padding
prop are checked for changes before re-rendering, Button
will still keep getting re-rendered on every click because onClick
is not memoized.
The first thing that would come to your mind if you used hooks for a while is to try useCallback
like this:
const memoizedCallback = React.useCallback(() => {
setCount(count + 1);
}, []);
Then pass it to the component:
<Button onClick={memoizedCallback} padding={8}>
Increment
</Button>
Unfortunately this doesn’t work too because count
gets trapped in the closure and only updates to 1. So we are getting the same behavior we had with useMemo
.
Currently, the only way I have seen and has been suggested to me is to switch
Button
back to sCU then memoize in the constructor. This was what we saw earlier. I will be glad if you have found a way to walk around this. Please feel free to tweet your hack at me
More useMemo Example
The fact that useMemo
couldn’t help us to control re-rendering in this scenario does not mean it does not have its own strengths. In fact, see the next section, Memoizing with useCallback, to see a difference scenario where useMemo
can memoize a callback. useMemo
actually shines more when you need to memoize heavy computations that would return the same result when given the same value or set of values.
I can’t give you a better example than Gabe Ragland’s
Brian Holt also made a set of examples to demonstrate all the common hooks API. Have a look at what he is doing with useMemo
and useCallback
Memoizing with useCallback
Consider we had another counter that does not rely on count
, for example:
function App() {
const [count, setCount] = React.useState(0);
const [anoutherCount, setAnotherCount] = React.useState(0); return (
<div class="App">
<Counter count={count} />
<Button
onClick={() => {
setCount(count + 1);
}}
padding={8}
>
Increment
</Button>
<Button
padding={8}
onClick={() => {
setCount(count - 1);
}}
>
Decrement
</Button>
<Counter count={anoutherCount} /> <Button onClick={() => { setAnotherCount(anoutherCount + 1); }} padding={8} > Increment </Button> <Button onClick={() => { setAnotherCount(anoutherCount - 1); }} padding={8} > Decrement </Button> </div>
);
}
The new counter (anotherCount
)and its increment and decrement button are entirely unrelated to the 1st counter. But what do you think will happen when we click any of the four buttons?
Since the states are in App
, if we increment or decrement count
or anotherCount
, the entire App
component and its descendants will get re-rendered.
If we memoize the count
buttons’ callbacks, we will be able to make the 2 buttons not to re-render since Button
is already a pure component through React.memo
:
function App() {
const [count, setCount] = React.useState(0);
const [anoutherCount, setAnotherCount] = React.useState(0);
const incrementMemoizedCallback = React.useCallback(() => { setCount(count + 1); }, [count]); const decrementMemoizedCallback = React.useCallback(() => { setCount(count - 1); }, [count]);
return (
<div class="App">
<Counter count={count} />
<Button
onClick={incrementMemoizedCallback} padding={8}
>
Increment
</Button>
<Button
padding={8}
onClick={decrementMemoizedCallback} >
Decrement
</Button>
<Counter count={anoutherCount} />
<Button
onClick={() => {
setAnotherCount(anoutherCount + 1);
}}
padding={8}
>
Increment
</Button>
<Button
onClick={() => {
setAnotherCount(anoutherCount - 1);
}}
padding={8}
>
Decrement
</Button>
</div>
);
}
What we are telling React to do to count
buttons is:
“Hey, when App
starts rendering, check if count
has changed and only re-render the count
buttons if count
was updated. If only anotherCount
changed, please ignore re-rendering these buttons.”
The memoized callbacks (with useCallback
) relies on the count
passed as the second argument to update the internal state of the callback function. If you don’t pass count
in, we will have the issue we experienced earlier where count
only makes it to 1.
You can even make the Counter
component a pure component with memo
and the first Counter
will not re-render when anotherCount
’s buttons are clicked.
It’s important to note that:
useCallback(() => {}, [dep,])
is the same as:
useMemo(() => () => {}, [dep,])
Therefore we can re-write one of our memoized functions like:
const incrementMemoizedCallback = React.useMemo(() => () => {
setCount(count + 1);
}, [count]);
Here is a live demo of that.
Memoizing with useRef
useRef
was primarily intended to act like React class refs. By intent, refs are meant to help you imperatively access child DOM properties.
In an attempt to achieve this in hooks, it ended up becoming a very useful API that not only accesses the DOM but:
- Stores data
- Does not cause re-render when the data it stores changes
- Remembers its stored data even after state change in
useState
causes a re-render.
Cool, right? Well, let me show you an example.
Back to our counter app, let’s try to re-write decrementMemoizedCallback
, to use useRef
instead of useCallback
. Start with declaring:
const decrementMemoizedCallback = React.useRef();
useRef
takes an initial value but when it’s not supplied, it can be empty so you can set it later.
Next, you need to find a way to set the callback function to decrementMemoizedCallback
only when count
is changed. This is where useEffect
comes to play:
const decrementMemoizedCallback = React.useRef();
React.useEffect(() => {
return decrementMemoizedCallback.current = () => {
setCount(count + 1);
}
}, [count]);
You can see the updated demo below:
Try to observe the flashes closely when you test with React Dev Tool:
See how the first Decrement
button flashes once, then stops. This is because the number 1 rule of useEffect
is that it has to run at least once. This is why the decrement callback in useEffect
was passed down even when count
has not changed. However, subsequent interactions does not affect the first Decrement
button.
This makes useRef
not as effective as useCallback
and useMemo
when it comes to memozing. That said, useRef
is still a powerful API when used properly and considering the above features I listed above that useRef
offers. When in doubt, fall back to useCallback
and useMemo
— it is as simple as that.
Utility Hook APIs are Awesome
Your first week with hooks might just be with useState
and useEffect
but if you give hooks more time, you will realize that more advanced powers lies in its utility APIs like useMemo
, useRef
and useCallback
. That said, spend some time to dig the React Hooks docs for more hidden gems. You can also play with all the examples at useHooks.com — they will get you more excited about the tips and tricks of what I like to call utility hooks.

Written by Christian Nwamba.
Follow on Twitter