Alex Sidorenko

A Visual Guide to React Rendering - useCallback

August 11, 2021

This article is a 4th chapter of "A Visual Guide to React Rendering." Previous chapters: It always re-renders, Props, and useMemo.

You can often see an event handler passed to a React component as an anonymous function. This will cause the child component to re-render when the parent renders, even if you wrap the child in memo. Let’s figure out why.

Two components Parent and Child. Parent holds a state - count: 0. Parent passes a prop 'onClick={() => setCount(count + 1)}' to the Child. Child re-renders every time Parent renders

Functions in Javascript

Javascript has First-class functions.

A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.

MDN - First-class Function

When you pass an anonymous function like that, it’s easy to overlook that onClick is just a prop of a component, and the function you pass is just the value of this prop. Let’s declare the variable with our function, so it’s easier to spot.

Two components Parent and Child. Parent holds a state - count: 0. Parent declares a variable 'handler' that stores value '() => setCount(count + 1)'. Parent passes a prop 'onClick={handler}' to the Child. Child re-renders every time Parent renders

This way, it’s a bit more obvious. The handler stores a value for onClick prop. Whenever this value changes, the Child re-renders. We know from the second chapter that functions are non-primitive values and are compared by reference.

const a = () => 1
const b = () => 1
a === b // false
a === a // true

Every time Parent renders, the handler redeclares with a new reference pointing to a new value. This causes the Child to re-render. If we want to prevent that, we need to provide the same reference to the onClick prop. And to do that, we need to memoize the value of the handler.

The useCallback

In the previous chapter, we explored useMemo and how it caches the value instead of recalculating it on every render. The useCallback is essentially the same. The only difference is that it returns a function.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

React Docs - useCallback”

So if we want to keep a reference for a handler, all we need to do is wrap its value in useCallback.

Two components Parent and Child. Parent holds a state - count: 0. Parent declares a variable 'handler' that stores value 'useCallback(() => setCount(count + 1), [])'. Parent passes a prop 'onClick={handler}' to the Child. Child doesn't re-render when parent renders. But the count only updates once

It stops the Child from re-rendering. But wait, why the count only updates once? That’s because of the dependency list. As with useMemo, useCallback will only recalculate its value when one of its dependencies changes. Since we have an empty list of dependencies, the handler value calculates only on the first render. And because of the closure, the memoized function will refer to the old value of count, even when the count changes. In our case, the count will always be 0. Therefore count + 1 will always be 1.

If you are not sure what the closure is, check out this article - MDN -Closures. There is a lot to take in. Consume it in portions. It can be hard to wrap your head around at first, but understanding this concept will give you React superpowers.

So how do we make the count update? We can put the count in the list of dependencies. This way, useCallback will recalculate the handler value every time the count changes and return the updated function with the latest lexical scope.

Two components Parent and Child. Parent holds a state - count: 0. Parent declares a variable 'handler' that stores value 'useCallback(() => setCount(count + 1), [count])'. Parent passes a prop 'onClick={handler}' to the Child. Child re-renders every time Parent renders

But now we are back when we started. The Child keeps re-rendering because the handler changes every time the count changes. To solve that, we can use functional updates.

Functional updates

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value. Here’s an example of a counter component that uses both forms of setState:

React Docs - Functional Updates

 

// State update
setCount(count + 1)

// Functional state update
setCount(prevCount => prevCount + 1)

The functional update allows us to remove count from the dependency list without worrying about closures. The handler will not be recalculated every time the count changes. And the prevCount will always refer to the latest value.

Two components Parent and Child. Parent holds a state - count: 0. Parent declares a variable 'handler' that stores value 'useCallback(() => setCount(prevCount => prevCount + 1), [count])'. Parent passes a prop 'onClick={handler}' to the Child. Child re-renders every time Parent renders

Next chapter

A Visual Giude to React Rendering - Context

Want to get better at modern React?

Subscribe to get one short article delivered to your inbox every week

One article a week. No spam.
Unsubscribe any time