It’s critical for developers to create apps that function well. A one-second delay in load time can result in a 26% drop in conversion rates, research by Akamai has found. React memoization is the key to a faster client experience—at the slight expense of using more memory.
Memoization is a technique in computer programming in which computational results are cached and associated with their functional input. This enables faster result retrieval when the same function is called again—and it’s a foundational plank in React’s architecture.
React developers can apply three types of memoization hooks to their code, depending on which portions of their applications they wish to optimize. Let’s examine memoization, these types of React hooks, and when to use them.
Memoization in React: A Broader Look
Memoization is an age-old optimization technique, often encountered at the function level in software and the instruction level in hardware. While repetitive function calls benefit from memoization, the feature does have its limitations and should not be used in excess because it uses memory to store all of its results. As such, using memoization on a cheap function called many times with different arguments is counterproductive. Memoization is best used on functions with expensive computations. Also, given the nature of memoization, we can only apply it to pure functions. Pure functions are fully deterministic and have no side effects.
A General Algorithm for Memoization
Memoization always requires at least one cache. In JavaScript, that cache is usually a JavaScript object. Other languages use similar implementations, with results stored as key-value pairs. So, to memoize a function, we need to create a cache object and then add the different results as key-value pairs to that cache.
Each function’s unique parameter set defines a key
in our cache. We calculate the function and store the result (value
) with that key
. When a function has multiple input parameters, its key
is created by concatenating its arguments with a dash in between. This storage method is straightforward and allows quick reference to our cached values.
Let’s demonstrate our general memoization algorithm in JavaScript with a function that memoizes whichever function we pass to it:
// Function memoize takes a single argument, func, a function we need to memoize.
// Our result is a memoized version of the same function.
function memoize(func) {
// Initialize and empty cache object to hold future values
const cache = {};
// Return a function that allows any number of arguments
return function (...args) {
// Create a key by joining all the arguments
const key = args.join(‘-’);
// Check if cache exists for the key
if (!cache[key]) {
// Calculate the value by calling the expensive function if the key didn’t exist
cache[key] = func.apply(this, args);
}
// Return the cached result
return cache[key];
};
}
// An example of how to use this memoize function:
const add = (a, b) => a + b;
const power = (a, b) => Math.pow(a, b);
let memoizedAdd = memoize(add);
let memoizedPower = memoize(power);
memoizedAdd(a,b);
memoizedPower(a,b);
The beauty of this function is how simple it is to leverage as our computations multiply throughout our solution.
Functions for Memoization in React
React applications usually have a highly responsive user interface with quick rendering. However, developers may run into performance concerns as their programs grow. Just as in the case of general function memoization, we may use memoization in React to rerender components quickly. There are three core React memoization functions and hooks: memo
, useCallback
, and useMemo
.
React.memo
When we want to memoize a pure component, we wrap that component with memo
. This function memoizes the component based on its props; that is, React will save the wrapped component’s DOM tree to memory. React returns this saved result instead of rerendering the component with the same props.
We need to remember that the comparison between previous and current props is shallow, as evident in React’s source code. This shallow comparison may not correctly trigger memoized result retrieval if dependencies outside these props must be considered. It is best to use memo
in cases where an update in the parent component is causing child components to rerender.
React’s memo
is best understood through an example. Let’s say we want to search for users by name and assume we have a users
array containing 250 elements. First, we must render each User
on our app page and filter them based on their name. Then we create a component with a text input to receive the filter text. One important note: We will not fully implement the name filter feature; we will highlight the memoization benefits instead.
Here’s our interface (note: name and address information used here is not real):
Our implementation contains three main components:
-
NameInput
: A function that receives the filter information -
User
: A component that renders user details -
App
: The main component with all of our general logic
NameInput
is a functional component that takes an input state, name
, and an update function, handleNameChange
. Note: We do not directly add memoization to this function because memo
works on components; we’ll use a different memoization approach later to apply this method to a function.
function NameInput({ name, handleNameChange }) {
return (
<input
type="text"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
/>
);
}
User
is also a functional component. Here, we render the user’s name, address, and image. We also log a string to the console every time React renders the component.
function User({ name, address }) {
console.log("rendered User component");
return (
<div className="user">
<div className="user-details">
<h4>{name}</h4>
<p>{address}</p>
</div>
<div>
<img
src={`https://via.placeholder.com/3000/000000/FFFFFF?text=${name}`}
alt="profile"
/>
</div>
</div>
);
}
export default User;
For simplicity, we store our user data in a basic JavaScript file, ./data/users.js
:
const data = [
{
id: "6266930c559077b3c2c0d038",
name: "Angie Beard",
address: "255 Bridge Street, Buxton, Maryland, 689"
},
// —-- 249 more entries —--
];
export default data;
Now we set up our states and call these components from App
:
import { useState } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
import "./styles.css";
function App() {
const [name, setName] = useState("");
const handleNameChange = (name) => setName(name);
return (
<div className="App">
<NameInput name={name} handleNameChange={handleNameChange} />
{users.map((user) => (
<User name={user.name} address={user.address} key={user.id} />
))}
</div>
);
}
export default App;
We have also applied a simple style to our app, defined in styles.css
. Our sample application, up to this point, is live and may be viewed in our sandbox.
Our App
component initializes a state for our input. When this state is updated, the App
component rerenders with its new state value and prompts all child components to rerender. React will rerender the NameInput
component and all 250 User
components. If we watch the console, we can see 250 outputs displayed for each character added or deleted from our text field. That’s a lot of unnecessary rerenders. The input field and its state are independent of the User
child component renders and should not generate this amount of computation.
React’s memo
can prevent this excessive rendering. All we need to do is import the memo
function and then wrap our User
component with it before exporting User
:
import { memo } from “react”;
function User({ name, address }) {
// component logic contained here
}
export default memo(User);
Let’s rerun our application and watch the console. The number of rerenders on the User
component is now zero. Each component only renders once. If we plot this on a graph, it looks like this:
Additionally, we can compare the rendering time in milliseconds for our application both with and without using memo
.
These times differ drastically and would only diverge as the number of child components increases.
React.useCallback
As we mentioned, component memoization requires that props remain the same. React development commonly uses JavaScript function references. These references can change between component renders. When a function is included in our child component as a prop, having our function reference change would break our memoization. React’s useCallback
hook ensures our function props don’t change.
It is best to use the useCallback
hook when we need to pass a callback function to a medium to expensive component where we want to avoid rerenders.
Continuing with our example, we add a function so that when someone clicks a User
child component, the filter field displays that component’s name. To achieve this, we send the function handleNameChange
to our User
component. The child component executes this function in response to a click event.
Let’s update App.js
by adding handleNameChange
as a prop to the User
component:
function App() {
const [name, setName] = useState("");
const handleNameChange = (name) => setName(name);
return (
<div className="App">
<NameInput name={name} handleNameChange={handleNameChange} />
{users.map((user) => (
<User
handleNameChange={handleNameChange}
name={user.name}
address={user.address}
key={user.id}
/>
))}
</div>
);
}
Next, we listen for the click event and update our filter field appropriately:
import React, { memo } from "react";
function Users({ name, address, handleNameChange }) {
console.log("rendered `User` component");
return (
<div
className="user"
onClick={() => {
handleNameChange(name);
}}
>
{/* Rest of the component logic remains the same */}
</div>
);
}
export default memo(Users);
When we run this code, we find that our memoization is no longer working. Every time the input changes, all child components are rerendering because the handleNameChange
prop reference is changing. Let’s pass the function through a useCallback
hook to fix child memoization.
useCallback
takes our function as its first argument and a dependency list as its second argument. This hook keeps the handleNameChange
instance saved in memory and only creates a new instance when any dependencies change. In our case, we have no dependencies on our function, and thus our function reference will never update:
import { useCallback } from "react";
function App() {
const handleNameChange = useCallback((name) => setName(name), []);
// Rest of component logic here
}
Now our memoization is working again.
React.useMemo
In React, we can also use memoization to handle expensive operations and operations within a component using useMemo
. When we run these calculations, they are typically performed on a set of variables called dependencies. useMemo
takes two arguments:
- The function that calculates and returns a value
- The dependency array required to calculate that value
The useMemo
hook only calls our function to calculate a result when any of the listed dependencies change. React will not recompute the function if these dependency values remain constant and will use its memoized return value instead.
In our example, let’s perform an expensive calculation on our users
array. We’ll calculate a hash on each user’s address before displaying each of them:
import { useState, useCallback } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
// We use “crypto-js/sha512” to simulate expensive computation
import sha512 from "crypto-js/sha512";
function App() {
const [name, setName] = useState("");
const handleNameChange = useCallback((name) => setName(name), []);
const newUsers = users.map((user) => ({
...user,
// An expensive computation
address: sha512(user.address).toString()
}));
return (
<div className="App">
<NameInput name={name} handleNameChange={handleNameChange} />
{newUsers.map((user) => (
<User
handleNameChange={handleNameChange}
name={user.name}
address={user.address}
key={user.id}
/>
))}
</div>
);
}
export default App;
Our expensive computation for newUsers
now happens on every render. Every character input into our filter field causes React to recalculate this hash value. We add the useMemo
hook to achieve memoization around this calculation.
The only dependency we have is on our original users
array. In our case, users
is a local array, and we don’t need to pass it because React knows it is constant:
import { useMemo } from "react";
function App() {
const newUsers = useMemo(
() =>
users.map((user) => ({
...user,
address: sha512(user.address).toString()
})),
[]
);
// Rest of the component logic here
}
Once again, memoization is working in our favor, and we avoid unnecessary hash calculations.
To summarize memoization and when to use it, let’s revisit these three hooks. We use:
-
memo
to memoize a component while using a shallow comparison of its properties to know if it requires rendering. -
useCallback
to allow us to pass a callback function to a component where we want to avoid re-renders. -
useMemo
to handle expensive operations within a function and a known set of dependencies.
Should We Memoize Everything in React?
Memoization is not free. We incur three main costs when we add memoization to an app:
- Memory use increases because React saves all memoized components and values to memory.
- If we memoize too many things, our app might struggle to manage its memory usage.
-
memo
’s memory overhead is minimal because React stores previous renders to compare against subsequent renders. Additionally, those comparisons are shallow and thus cheap. Some companies, like Coinbase, memoize every component because this cost is minimal.
- Computation overhead increases when React compares previous values to current values.
- This overhead is usually less than the total cost for additional renders or computations. Still, if there are many comparisons for a small component, memoization might cost more than it saves.
- Code complexity increases slightly with the additional memoization boilerplate, which reduces code readability.
- However, many developers consider the user experience to be most important when deciding between performance and readability.
Memoization is a powerful tool, and we should add these hooks only during the optimization phase of our application development. Indiscriminate or excessive memoization may not be worth the cost. A thorough understanding of memoization and React hooks will ensure peak performance for your next web application.
The Toptal Engineering Blog extends its gratitude to Tiberiu Lepadatu for reviewing the code samples presented in this article.