Recently, I've been asked when to use the useMemo
hook, and this question made me think and reflect on it. I slowly realised that I fell into the habit of using the useMemo
hook for pretty much everything, and I couldn't explain why I was doing it. And especially what made me feel worried is that after a chat with another front-end engineer, I've realised I'm not the only one doing it. It means that developers tend to overuse the useMemo
hook and can't even adequately explain why they are doing it. In this post, we will learn when to use the useMemo
hook and when not.
Profile first, optimise second. Before optimising a component, use React's built-in tools to accurately profile performance and identify bottlenecks.
Memoise only expensive computations. Apply useMemo
only to genuinely resource-intensive computations that occur frequently.
Keep dependencies accurate. Double-check that all relevant dependencies are provided correctly to useMemo
to ensure it updates when needed.
Before delving into the overuse of useMemo
, let's briefly understand its purpose. The useMemo
hook is used to memoise the results of a function and returns a cached value that only changes when its dependencies change. This ensures that expensive computations are not repeated on every render, thus optimising the performance of React components.
useMemo
should be employed when dealing with computations that are time-consuming or resource-intensive, and the results don't change between renders. For instance, calculating the factorial of a number or formatting large datasets for display are suitable scenarios for using useMemo
.
After reflecting about my overuse of useMemo
hook, I realised that there are a few pitfalls that we should be aware of when using useMemo
hook.
useMemo
before identifying actual performance bottlenecks. It's essential to profile your application and identify which parts are genuinely causing performance issues before adding memoisation.useMemo
can lead to unexpected side effects if dependencies are not correctly managed. If dependencies are accurately provided, you might avoid situations where the cached value does not update when it should, leading to outdated or incorrect results.Okay, expensive computations were mentioned multiple times, but how can I understand what is considered expensive? Well, it's hard to say, depending on the context. For instance, if you are working with a large dataset and need to format it for display, this is considered an expensive computation. However, if you are working with a small dataset, then this is not considered an expensive computation. In other words, if the computation takes a long time to complete, it's considered expensive.
Let's take a look at the following example. We have NonMemoComponent
and MemoComponent
; in both, we use performance.now()
to measure the execution time of the expensiveCalculation
function. The only difference between these two components is that MemoComponent
uses the useMemo
hook to memoise the result of the expensiveCalculation
function. Let's see what happens when we click the button and increment the count in both cases.
1import React, { useState } from 'react';2
3const NonMemoComponent = () => {4 const [count, setCount] = useState(0);5
6 const expensiveCalculation = () => {7 let result = 0;8 for (let i = 0; i < 10000000; i++) {9 result += i;10 }11 return result;12 };13
14 const handleClick = () => {15 setCount(count + 1);16 };17
18 const startTime = performance.now();19 const result = expensiveCalculation();20 const endTime = performance.now();21
22 console.log('Non-memoised calculation execution time:', endTime - startTime);23
24 return (25 <div>26 <button onClick={handleClick}>Increment Count</button>27 <p>Count: {count}</p>28 <p>Result: {result}</p>29 </div>30 );31};32
33export default NonMemoComponent;
1import React, { useState, useMemo } from "react";2
3const MemoComponent = () => {4 const [count, setCount] = useState(0);5
6 const expensiveCalculation = useMemo(() => {7 const startTime = performance.now();8 // Simulate an expensive calculation9 let result = 0;10 for (let i = 0; i < 10000000; i++) {11 result += i;12 }13 const endTime = performance.now();14
15 console.log("Memoised calculation execution time:", endTime - startTime);16
17 return result;18 }, []); // Include 'count' as a dependency if you want to recompute when it changes.19
20 const handleClick = () => {21 setCount(count + 1);22 };23
24 return (25 <div>26 <button onClick={handleClick}>Increment Count</button>27 <p>Count: {count}</p>28 <p>Result: {expensiveCalculation}</p>29 </div>30 );31};32
33export default MemoComponent;
I've used CodeSandbox for this snippet, but to run this example, you must disable
Infinite Loop Protection
insandbox.config.json
.
If you open the console, you can see that Memo execution time: 0
. How come? You're seeing this in the console for the first render of the memoised component because of how the code is structured.
In the memoised component, the expensiveCalculation
is wrapped in a useMemo
hook with an empty dependency array. This means the calculation is performed only once when the component is initially rendered, and the result is memoised (cached) for subsequent renders. Since the calculation is done during the initial render, the memoised result is immediately available and doesn't require an additional execution time when you log in.
Here's a breakdown of what's happening:
useMemo
hook calculates the result of expensiveCalculation
during the initial render.This is a characteristic of how useMemo
works. It's designed to optimise expensive computations by calculating them only when necessary and then reusing the cached result for subsequent renders as long as the dependencies remain the same. In this case, the empty dependency array ensures that the calculation is done only once during the initial render.
Sign up to get updates when I write something new. No spam ever.
Subscribe to my Newsletter