As React apps grow bigger and more complex, performance becomes more and more of a problem. As components become larger and contain more and more sub-components, rendering becomes slow and turns into a bottleneck.
How do we tackle this? If you haven’t used useMemo
and useCallback
, we can start with those.
In this tutorial, we will take a look at how these 2 very easy and handy callbacks work, and why they are so useful. In fact, these days my eyes get sore when I don’t see them used. So let’s dive in to what they do.
React.useMemo
This React hook’s one goal is to save a value for later use, and not re-calculating it on the spot.
Let’s take an example of some expensive logic that runs in our render function:
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = list.map((i) => (i * 972 + 1000) / 5213).join(', ')
function addRandom() {
setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
}
function increaseCounter() {
setCounter((prev) => ++prev)
}
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
Doesn’t seem very problematic, but take a look at the multiplied
variable. The logic in this
example isn’t too bad, but imagine working with a giant list of special objects. This mapping alone
could be a performance problem, especially if it’s looped in a parent component.
In this case, there is another state hook - counter
. When setCounter
is called, multiplied
will be calculated all over again, wasting previous resources, even when an update in that case is
not needed as these variables are independent of each-other.
That’s where useMemo
comes in hand (read the
official docs).
You can use this hook to save the value, and retrieve the same object until a re-calculation is needed.
Here’s how it’s used, the only line we need to change is the multiplied
definition:
const multiplied = React.useMemo(() => {
console.log('recalculating multiplied:', list)
return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
The useMemo
hook takes 2 arguments:
- The
create
function - used to return the calculated value of the variable we want to eventually use - A list of dependencies. The dependency list is used to determine when a new value should be
calculated - i.e, when to run the
create
function again.
We added a console.log
call here just to note when a new value is being calculated.
And with those changes, we can try our component again (here is the updated code just in case):
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = React.useMemo(() => {
console.log('recalculating multiplied:', list)
return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
function addRandom() {
setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
}
function increaseCounter() {
setCounter((prev) => ++prev)
}
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
If you now change the counter by using the “Increase Counter” button, you will see our console.log
call is not being invoked again until we use the other button for “Add Random”.
React.useCallback
Now we have the other hook - useCallback
(read the
official docs).
This works exactly like the useMemo
hook - except it is for functions instead of variable values.
We can take our button functions, and wrap each in this hook to make sure our function reference only changes when needed.
const ExpensiveComponent: React.FC = (props) => {
const [list, setList] = React.useState([])
const [counter, setCounter] = React.useState(0)
const multiplied = React.useMemo(
() => list.map((i) => (i * 972 + 1000) / 5213).join(', '),
[list],
)
const addRandom = React.useCallback(
() => setList((prev) => [...prev, Math.floor(Math.random() * 10000)]),
[setList],
)
const increaseCounter = React.useCallback(() => setCounter((prev) => ++prev), [setCounter])
return (
<div>
Counter: {counter}
<br />
Multiplied: {multiplied}
<br />
<button onClick={addRandom}>Add Random</button>
<button onClick={increaseCounter}>Increase Counter</button>
</div>
)
}
Now both our variables and functions are memoized and will only change reference when their dependencies dictate they do.
Caveats
Using these hooks don’t come without their share of problems.
-
Consider whether this actually improves performance or not in your specific case. If your state is pretty regularly changed and these memoizations have to run pretty often, their performance increases might be outweighed by the performance costs of actually running the memoization logic.
-
Dependency checking and generating can be expensive. Be careful what you put in the dependency lists, and if needed, make some more memoization and map your objects and lists in deterministic ways so that they are statically inspectable easily. Also avoid using expensive methods such as
JSON.stringify
to create those memoizations or dependencies, as it might also be too expensive to be worth the trouble and might make things worse.
Other things to consider
You might want to make sure your project uses lint rules that enforce exhaustive dependencies, as they make tracking these things much more easy.
In some cases, you might want to add ignore comments in very specific places, but it makes it very clear that this part is built that way intentionally and prevents more confusion about when to update the dependencies.
Hopefully you find this useful. There are many other hooks to learn about, but these 2 are very
useful and often ignored, so I thought it would be good to highlight them. If you are interested you
might want to look up useRef
and how it differs from useMemo
, or maybe I’ll make another part
about it in the future. Who knows?
About the author
My name is Chen Asraf. I’m a programmer at heart — it's both my job that I love and my favorite hobby. Professionally, I make fully fledged, production-ready web, desktop and mobile apps for start-ups and businesses; or consult, advise and help train teams.
I'm passionate about tech, problem solving and building things that people love. Find me on social media: