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

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:

  1. Why do they need to remember?
  2. What do they remember?
  3. 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>

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:

  1. We have seen how to control re-rendering using sCU and PureComponent.
  2. 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:

  1. Functional components cannot have instance methods so no sCU
  2. Functional components cannot extend other classes so no PureComponent
  3. 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:

  1. Might cause re-render when re-rendering is not needed
  2. 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:

Data vs Memoize hooks

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

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:

  1. Stores data
  2. Does not cause re-render when the data it stores changes
  3. 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.