There are many optimization techniques you can use to improve overall performance of your React applications. One of these techniques is memoization. In this tutorial you will learn what memoization is and how to use memoization in React to optimize your React apps.
Memoization made simple
Memoization is one of the optimization techniques used in programming. It allows you to save time and resources by avoiding unnecessary computations. Computation is not necessary when the result of the computation is the same as the result of previous compilation.
Let's take a simple example. Imagine you have a function that returns factorial of a given number. Usually, this function will will run a computation for every number you give it. Is this necessary? For example, let's say you run the function two or three times with the same number.
Is in this case necessary to run through the whole computation to return a value this function have already seen in the past? No. What you can do instead, to prevent this, is to create a cache and modify the function. Each time the function runs it will first look inside the cache.
If the number you gave the function is already in cache there is no need to calculate anything. That factorial function can simply return the known result for that number. If the number is not in cache, factorial function can do its job and calculate the factorial, and add it to the cache.
// Create cache:
let cache = [1]
// Create memoized factorial function:
function getFactorialMemoized(key) {
if (!cache[key]) {
// Add new value to cache:
cache[key] = key * getFactorialMemoized(key - 1)
} else {
// Return cached value:
console.log('cache hit:', key)
}
// Return result
return cache[key]
}
getFactorialMemoized(6)
getFactorialMemoized(6)
This example demonstrates what memoization is basically about. You compute some values and store them, memoize them, for later use. If, in some time in the future, you need to get one of those values, you don't have to compute them again. Instead, you retrieve them from your storage, some cache.
As you can probably guess, this technique can bring significant performance improvements. It is usually much faster and resource-friendly to simply return some value instead of computing it. This sounds great, but how can you use memoization in React?
Memoization in React
Good news is that React provides built-in tools for memoization out of the box. This means that you don't have to add any extra dependencies. The only dependency you need is react, and react-dom. Memoization tools React provides at this moment are three: memo()
, useMemo()
and useCallback()
.
Memo
The first tool for memoization in React is a higher order component called memo()
. What high order component does is it takes one React component and returns new. With memo()
, there is one important difference. This new returned component is also memoized.
This means that React will not re-render this memoized component unless it is necessary to update it. What this means is that as long as the component props stay the same React will skip re-rendering the memoized component. It will instead keep reusing the result of the last render.
When React detects that some component prop has changed it will re-render the component. This is to ensure that the UI is kept up-to-date and synchronized. When it comes to memo()
, there are two important things to mention.
// Import memo
import { memo } from 'react'
// Component without memo:
export const App = () => {
return (
<div>
<h1>This is a normal component</h1>
</div>
)
}
// Component wrapped with memo:
export const App = memo(() => {
return (
<div>
<h1>This is a memoized component</h1>
</div>
)
})
Local states
The first thing is that React will watch only for changes of props. It doesn't watch for changes in logic inside the component. It will also not prevent these changes from re-rendering the component. One example of such a change is if that component has its own local state.
When local state changes, the component will still re-render. This is by design to ensure the UI and date are in sync. This also applies to components connected to providers or redux stores. Change in these data entities will result in re-renders of components that are connected to them.
Let's take a look at a simple example. Imagine you have a component that tracks the number of counts. It renders current count and button to increment the count by 1. Even though the component itself is memoized, each click on the button will result in re-render.
What is important to remember is that this is not a bug, but a feature. React re-renders the component to keep the rendered count value in sync with the data in component's local state. Without re-renders, the rendered number would stay stuck on 0.
// Import memo and useState:
import { memo, useState } from 'react'
export const App = memo(() => {
// Create local state:
const [count, setCount] = useState(0)
// This will log on every re-render:
console.log('Render')
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
return (
<div>
<h1>Current count: {count}</h1>
<button onClick={onCountClick}>Click me</button>
</div>
)
})
Shallow comparison
The second thing is that React does only shallow comparison of props for memoized components. This may not be enough if you pass through props more complex data than primitive data types. In that case, memo()
HOC also allows to pass your own custom compare function as a second argument.
This custom compare function has two parameters, previous and next props. Inside this function you can execute any custom comparison logic you need.
// Import memo and lodash:
import { memo } from 'react'
import { isEqual } from 'lodash'
// Create custom comparison function:
function isEqual(prevProps, nextProps) {
// Return result of some custom comparison:
return isEqual(prevProps, nextProps)
}
// Component wrapped with memo:
export const App = memo(() => {
return (
<div>
<h1>This is a memoized component</h1>
</div>
)
}, isEqual) // Pass custom comparison function
useMemo
The second tool that helps with memoization in React is React hook useMemo(). Unlike memo()
, the useMemo
hook allows you to execute some computation and memoize its result. Then, as long as the input it watches stays the same, useMemo()
will return the cached result, avoiding unnecessary computation.
A simple example
For example, imagine some components gets a number through props. It then takes this number and calculates its factorial. This is the difficult computation we want to optimize with memoization. The component also has a local state. It can the count tracker we've already played with.
We will add function to calculate factorial and use this function to calculate factorial and assign the result to regular variable. What will happen? The factorial will be computed when the component mounts. The problem is that it will be also computed when we click the count button and increment count.
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = ({ number }) => {
// Create local state:
const [count, setCount] = useState(0)
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Create factorial function:
const getFactorial = (num) => {
// Print log when function runs:
console.log('count factorial')
// Return the factorial:
return num === 1 ? num : num * getFactorial(num - 1)
}
// Calculate factorial for number prop:
const factorial = getFactorial(number)
// THIS ^ is the problem.
// This variable will be re-assigned,
// and factorial re-calculated on every re-render,
// every time we click the button to increment count.
return (
<div>
<div>Count: {count}</div>
<div>Factorial: {factorial}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
In the example above, we can see that factorial is recalculated because every time we click the button the log inside getFactorial()
is printed in the console. This means that every time the button is clicked, the getFactorial()
function is executed, even though the number in props is the same.
A simple solution
We can quickly solve this problem with the help of useMemo()
hook. All we have to do is wrap the call of getFactorial()
function with useMemo()
. This means that we will assign the factorial
variable with useMemo()
hook and pass the getFactorial()
function into the hook.
We should also make sure the factorial will be re-calculated when the number passed through props changes. To do this, we specify this prop as a dependency we want to watch in useMemo()
hook dependency array.
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = ({ number }) => {
// Create local state:
const [count, setCount] = useState(0)
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Create factorial function:
const getFactorial = (num) => {
// Print log when function runs:
console.log('count factorial')
// Return the factorial:
return num === 1 ? num : num * getFactorial(num - 1)
}
// Calculate and memoize factorial for number prop:
const factorial = useMemo(() => getFactorial(number), [number])
// 1. Wrap the getFactorial() function with useMemo
// 2. Add the "number" to dependency array ("[number]") to tell React it should watch for changes of this prop
return (
<div>
<div>Count: {count}</div>
<div>Factorial: {factorial}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
Thanks to this simple change we can prevent unnecessary computations that could otherwise slow down our React app. This way, we can memoize any computation we need. We can also use useMemo()
multiple times to ensure calculations on re-renders are really minimized.
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = () => {
// Add state to force re-render
const [count, setCount] = useState(0)
// Add button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Add some dummy data and memoize them:
const users = useMemo(
() => [
{
full_name: 'Drucy Dolbey',
gender: 'Male',
},
{
full_name: 'Ewart Sargint',
gender: 'Male',
},
{
full_name: 'Tabbi Klugel',
gender: 'Female',
},
{
full_name: 'Cliff Grunguer',
gender: 'Male',
},
{
full_name: 'Roland Ruit',
gender: 'Male',
},
{
full_name: 'Shayla Mammatt',
gender: 'Female',
},
{
full_name: 'Inesita Eborall',
gender: 'Female',
},
{
full_name: 'Kean Smorthit',
gender: 'Male',
},
{
full_name: 'Celestine Bickerstaff',
gender: 'Female',
},
],
[]
)
// Count female users and memoize the result:
const femaleUsersCount = useMemo(
() =>
users.reduce((acc, cur) => {
console.log('Invoke reduce')
return acc + (cur.gender === 'Female' ? 1 : 0)
}, 0),
[users]
)
return (
<div>
<div>Users count: {femaleUsersCount}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
In the example above, memoizing the result of femaleUsersCount
assignment is not enough. We have to memoize the users
as well. Otherwise, users
variable would be re-assigned every time the component re-renders. This would also trigger useMemo()
for the femaleUsersCount
. This would mean that nothing is actually memoized.
When we memoize users
we prevent it from re-assigning. This will prevent unnecessary change of users
, and consequently, of femaleUsersCount
. As a result, only count
will change. Well, actually, the onCountClick()
will be re-created as well. This brings us to the last tool for memoization in React.
useCallback
We can do a lot with memo()
and useMemo()
to use memoization in React to avoid unnecessary computation of various kinds. There is still one problem we haven't covered yet. Every time component re-renders it also re-creates all local functions. This is a double-edged sword.
Two issues with re-created functions
It is a double-edged sword because it can lead to two issues. First, all functions you declare in a component will be re-created on every render. This may or may not have significant impact, depending on how many functions you usually have. The second issue can cause more problems.
Simple example. Let's say you have one parent and one child component. Parent component creates a local state and function. That function also gets passed to the child through props so it can be used there. Problem? Do you remember that thing about memo()
and shallow comparison?
The thing is that when you pass a function to component you are passing complex value, not primitive. React's shallow comparison will fail here. It will tell you the value is different and re-render the component even though the value is the same. In our case, the value is the function.
When parent component re-renders, it also re-creates the function it passes to the child component. When re-created function gets passed, React fails to recognize that the function, even though newly created, is actually the same as the previous.
The result of this is that the child component will re-render as well. This will simply happen, whether you use memo()
or not.
// Child component:
import { memo } from 'react'
export const CountChild = memo((props) => {
console.log('CountBox render')
return <button onClick={props.onChildBtnClick}>Click me as well</button>
})
// Parent component:
import { useState, memo, useCallback } from 'react'
// Import child component
import { CountChild } from './countChild'
export const App = memo(() => {
// Add state to force re-render
const [count, setCount] = useState(0)
// Add button handler:
const onCountClick = () => {
setCount((prevCount) => ++prevCount)
}
return (
<div>
<div>count: {count}</div>
<button onClick={onCountClick}>Click me</button>
<CountBox onChildBtnClick={onCountClick} />
</div>
)
})
Avoiding re-renders caused by functions passed through props
Way to avoid this is by using the useCallback() hook. Instead of declaring a function as usually, we can pass it as a callback to useCallback()
hook and assign it to a variable. This, and properly set array dependencies, will ensure that the function will be re-created only when necessary.
This means only when one of the dependencies changes. When re-render happens and if no dependency change, React will use cached version of the function instead of re-creating it. React returning cached version of the function will also prevent the child component from unnecessary re-render.
This is because React knows the function is cached, and thus the same. So, unless some other prop has changed, there is no need to re-render the child component.
// Child component:
import { memo } from 'react'
export const CountChild = memo((props) => {
console.log('CountBox render')
return <button onClick={props.onChildBtnClick}>Click me as well</button>
})
// Parent component:
import { useState, memo, useCallback } from 'react'
// Import child component
import { CountChild } from './countChild'
export const App = memo(() => {
// Add state to force re-render
const [count, setCount] = useState(0)
// CHANGE: Memoize the button handler:
const onCountClick = useCallback(() => {
setCount((prevCount) => ++prevCount)
}, []) // No dependency is needed
return (
<div>
<div>count: {count}</div>
<button onClick={onCountClick}>Click me</button>
<CountBox onChildBtnClick={onCountClick} />
</div>
)
})
Conclusion: Memoization in React
Thanks to memo()
, useMemo()
and useCallback()
memoization in React is quite easy. With these tools, we can make our React applications faster and better. I hope that this tutorial helped you understand what memoization is and how to use memoization in React to optimize your React apps.
Top comments (0)