This article was last updated on January 16, 2024 to reflect the latest changes to the React memo API and to provide a more detailed explanation of how React.memo() works.
Introduction
This post introduces the React Memoization Series and demonstrates the usage of the React.memo
API. React.memo
memoizes a functional component and its props. Doing so helps prevent unnecessary re-renderings that originate from the re-renderings of the component's parent / ancestors.
This is the first post of a three-part series hosted on Refine blog on the use of memoization in React.
The other two posts in the series cover the usage of React useMemo()
and useCallback()
hooks.
Steps we'll cover in this post:
- What is Memoization?
- Why Memoization in React?
- Memoization in React
- About the React Memoization Series
- Memoizing a Functional Component using
React.memo()
What is Memoization?
Memoization is an performance optimization technique that allows us to minimize the use of memory and time while executing a resource-intensive function. It works by storing the last computed value or object from the function. Memoization lets us bypass the function's costly computations when the function is called with the same parameters repeatedly.
Why Memoization in React?
Memoization plays a crucial role in enhancing the performance of a React component. It addresses following shortcomings in React:
Excessive Re-rendering Due to Ancestor Re-rendering
React is all about re/rendering components in the virtual DOM prior to updating the actual Document Object Model in the browser. Re-render in an ancestor component, by default, triggers a re-render in a descendent component.
For example, a local state update in a parent component causes it to re-render. This, in turn, causes its children to re-render.
Such behavior in React causes a lot of memory and time to be wasted on useless renderings of the descendent components. Excessive re-renderings, therefore impact a React app's performance negatively.
Expensive Utilities
In addition, resource intensive functions such as utilities used in data processing, transformation and manipulation lower a React app's performance. Functions used for sorting, filtering and mapping traverse large sets of data and therefore slows down an application.
Passing Callbacks to Children
Performance of a React app is also adversely effected due to callback functions passed from a parent component to a child. This happens because a new function object from the callback is created in memory every time the child re-renders. So, multiple copies of the same callback function are spun off in runtime and they consume resources unnecessarily.
Memoization in React
Using memoization the right way in React helps in mitigating these drawbacks and facilitates better use of computing resources in a React app.
Memoization can be used in a number of ways for optimizing the performance of a React app. React components can be memoized to prevent unnecessary component re-renders originating from ancestors participating in the component hierarchy. In functional React, component memoization is done using the React.memo
API.
Caching values of expensive utility functions and memoizing callbacks are two common ways of boosting a React app's performance. Caching function values is done using useMemo()
hook. And callback functions are memoized with the useCallback()
hook.
About the React Memoization Series
The React Memoization Series is a three part guide on how to implement memoization in a React app. Each part demonstrates in the browser console how memoization contributes to performance optimization.
The three parts are:
- React Memo Guide with Examples
- React useMemo Hook Guide With Examples
- Memoization in React - How useCallback Works
In the first post, we implement memoizing a React component with React.memo()
and demonstrate how unnecessary re-renders coming from ancestor state updates are prevented. The second post covers how caching the value of an expensive utility function with useMemo
stops repetitive invocations of data heavy computations that slow down a React app. In the third part, we get an idea on how memoization of callbacks passed to child components helps reduce application memory consumption.
We will begin with an example that involves memoizing a functional component with React.memo()
. In the subsequent posts, we will gradually extend it to include use cases for the useMemo()
and useCallback()
hooks.
Project Overview
This series is a demo rather than a step-by-step coding tutorial. It is intended to demonstrate how memoization contributes to performance optimization in a React app. We've made the code available here.
All the components have been already coded. We'll be showing how memoization is implemented using React.memo
, useMemo()
and useCallback
APIs by examining relevant code snippets and highlighting lines on the existing components.
We'll follow the impact of memoization mainly from the browser's console.
Setup
In order to properly follow this tutorial, we recommend you run the app in a browser - since we will be visiting the console to investigate the impact of memoization on our React app.
For this to happen, please follow the below steps as outlined here:
- Clone this repository.
- Open it in your code editor and install the packages:
yarn install
- Then run the app:
yarn start
- Open Google Chrome and navigate to
http://localhost:3000
. - Use
CTRL
+Shift
+J
on Ubuntu orCommand
+Option
+J
on Mac to inspect the webpage and open browser's console.
Investigation
If you look at the project folder in your code editor, you'll find that react-memoization
is created using create-react-app
.
The app is based on the idea of a list of posts on a blog. There are several components involving a user presented the latest posts and a list of the user's posts. Allow yourself some time to understand the components individually, their relationships, their state changes, and how props are passed through. It is crucial to pay close attention to how the change of a parent's state triggers re-render of its descendants.
Let's dig into the components and check out what's happening.
The <App />
Component
To begin with, we have an <App />
component that houses <Blog />
.
If we look inside <App />
, we can see that we're storing a signedIn
state with useState()
hook. We also have a toggler function that alters the value of signedIn
:
import { useState } from "react";
import Blog from "./components/Blog";
function App() {
const [signedIn, setSignedIn] = useState(false);
const handleClick = () => setSignedIn(!signedIn);
console.log("Rendering App component");
return (
<main>
<nav>
<button onClick={handleClick}>Sign Out</button>
</nav>
<Blog signedIn={signedIn} setSignedIn={setSignedIn} />
</main>
);
}
export default App;
In the JSX, we pass signedIn
to <Blog />
.
The <Blog />
Component
Looking inside <Blog />
, it fetches a list of posts with a click on the Get Latest Post
button and sets the updatedPosts
state:
import React, { useEffect, useMemo, useState } from "react";
import fetchUpdatedPosts from "../fetch/fetchUpdatedPosts";
import allPosts from "./../data/allPosts.json";
import sortPosts from "../utils/sortPosts";
import LatestPost from "./LatestPost";
import UserPostsIndex from "./UserPostsIndex";
const Blog = ({ signedIn }) => {
const [updatedPosts, setUpdatedPosts] = useState(allPosts);
const [localTime, setLocalTime] = useState(new Date().toLocaleTimeString());
const getLatestPosts = () => {
const posts = fetchUpdatedPosts();
setUpdatedPosts(posts);
};
const sortedPosts = sortPosts(updatedPosts);
useEffect(() => {
const id = setInterval(
() => setLocalTime(new Date().toLocaleTimeString()),
1000,
);
return () => clearInterval(id);
}, []);
console.log("Rendering Blog component");
return (
<div>
<div>{localTime}</div>
<button onClick={getLatestPosts}>Get Latest Post</button>
<LatestPost signedIn={signedIn} post={sortedPosts[0]} />
<UserPostsIndex signedIn={signedIn} />
</div>
);
};
export default Blog;
We can see that the updatedPosts
are sorted with the sortPosts
utility and the first item from the sorted array is then passed to <LatestPost />
component along with signedIn
.
The <LatestPost />
Component
Then coming to <LatestPost />
, it nests the <Post />
component, which we are going to memoize with React.memo()
.
Let's quickly run through <LatestPost />
to see what it does:
import React, { useEffect, useState } from "react";
import Post from "./Post";
const LatestPost = ({ signedIn, post }) => {
const [likesCount, setLikesCount] = useState(null);
useEffect(() => {
const id = setInterval(() => {
setLikesCount((likesCount) => likesCount + 1);
}, 3000);
return () => clearInterval(id);
}, []);
console.log("Rendering LatestPost component");
return (
<div>
{post ? (
<>
<Post signedIn={signedIn} post={post} />
{likesCount && (
<div className="my-1 p-1">
<span>{likesCount} Likes</span>
</div>
)}
</>
) : (
<p>Click on Get Latest Post button</p>
)}
</div>
);
};
export default LatestPost;
We can see that <LatestPost />
changes its local state of likesCount
every 3 seconds in the useEffect()
hook. Because of this, <LatestPost />
should re-render every 3 seconds. So should <Post />
as a consequence of being a child of <LatestPost />
:
The Post />
Component
Let's now focus on <Post />
. It receives signedIn
and post
as props and displays the content of post
:
import React from "react";
const Post = ({ signedIn, post }) => {
console.log("Rendering Post component");
return (
<div className="">
{post && (
<div className="post p-1">
<h1 className="heading-sm py-1">{post.title}</h1>
<p>{post.body}</p>
</div>
)}
</div>
);
};
export default Post;
Notice we are logging to the console the event when <Post />
gets rendered: console.log('Rendering Post component');
When we check the console, we can expect to see that <Post />
is re-rendered with a change in likesCount
from <LatestPost />
. This would be happening even though <Post />
does not depend on likesCount
.
If we examine closely, we can see that this is indeed the case: we have <Post />
rendering again and again following an interval:
Notice, rendering <Post />
is accompanied by <LatestPost />
at 3 seconds interval, so it is consistent that <Post />
's re-renders are happening due to likesCount
state changes in <LatestPost />
. That is, they are coming at 3000ms
intervals from <LatestPost />
's useEffect()
hook.
All these re-renders are futile for <Post />
and costly for the app. So we are going to prevent them using component memoization.
Memoizing a Functional Component using React.memo()
Now, if we memoize <Post />
with React.memo()
, the re-renders should stop.
So, in <Post />
, let's update the component export with the highlighted code:
const Post = ({ signedIn, post }) => {
console.log('Rendering Post component');
return ( ... );
};
export default React.memo(Post);
Looking at the console, we can see that Post
is no longer re-rendered at 3s intervals:
It is clear that memoizing <Post />
reduces the number of re-renders. In a real app, this is a huge blessing because re-renders due to frequent likes turn out to be very costly for a social media app's performance.
But what exactly happened?
What is React.memo
?
Well, with export default React.memo(Post);
, we produced a new component that re-renders only when its props and internal state is changed.
React.memo()
is a Higher Order Component (HOC) that memoizes the passed in component along with the value of its props. Doing so helps in optimizing its performance by preventing unnecessary re-renders due to changes it does not depend on, e.g. the unrelated state changes in ancestor components.
React.memo
does this by memoizing the component function itself and the accepted props. When the values of the props change, the component re-renders.
React.memo() - How to Memoize Component Props
We can see that <Post />
receives signedIn
and post
props.
Now, unlike with likesCount
, <Post />
depends on signIn
and post
. And React memo caches these props and checks for incoming changes in them. Incoming changes to them triggers a re-render. So, altering any of signedIn
or post
re-renders Post
.
If we look back inside <App />
, we see that signedIn
originated from there and gets relayed via <Blog />
and <LatestPost />
to <Post />
as props. We have a button in the navbar that toggles the value of signedIn
:
<nav className="navbar">
<button className="btn btn-danger" onClick={handleClick}>
Sign Out
</button>
</nav>
In the browser, let's try toggling its value to see the effect on memoized <Post />
.
Add the following console log statement to <Post />
in order to log the value of signedIn
to the console:
console.log(signedIn);
When we click on the Sign Out
button in the navbar, we can see in the console that <Post />
re-renders after <LatestPost />
:
This is now because React memo caches the props passed to the component and checks for incoming changes. Notice the Boolean value of signedIn
printed to the console. A change in signedIn
's state renews the memoization and a re-render of the component is triggered.
When to Use React.memo
This is actually what we want. Because we don't want <Post />
to re-render when we don't need it to, and we want to re-render it when we need it to.
If value of signedIn
never changed, we know <Post />
will never be re-rendered because of signedIn
. In that case, caching signedIn
doesn't do us any favor.
So, typically we should use React.memo
when we want to prevent re-renderings due to state changes that do not concern our component and only allow re-renderings due to prop changes that happen often or are driven by an event.
When Not to Use React.memo
In our example, had we resorted to React.memo()
solely to retain the value of signedIn
and not to prevent re-renders due to changes in likesCount
or post
, we would not get much performance benefit.
Instead, we would be bringing the comparison function into the scene for no reason, which adds to the performance cost. So, it is not recommended to memoize a component if its prop values don't change often.
It is therefore important to figure out the performance gains by measuring and analyzing runtime performance using browser utilities like Chrome DevTools.
React.memo: Prop Comparison
React memo checks for changes between the previous and current values for a given prop passed to the component. The default function carries out a shallow comparison on each passed in prop. It checks for equality of incoming values with the existing ones.
In our React.memo(Post)
memo, the current states of signedIn
and post
are checked for equality to their incoming states. If both values for each prop are equal, the memoized value is retained and re-render prevented. If they are not equal, the new value is cached and <Post />
re-renders.
Using React Memo with Custom Comparators
It is possible to customize the comparison by passing in a comparator function to the React.memo()
HOC as a second argument:
React.memo(Post, customComparator);
For example, we can specify dependencies for React.memo()
and choose to compare only the props we want to:
import React from "react";
const Post = ({ signedIn, post }) => {
console.log("Rendering Post component");
return ( ... );
};
const customComparator = (prevProps, nextProps) => {
return nextProps.post === prevProps.post;
};
export default React.memo(Post, customComparator);
Here, we are omitting signedIn
from the comparison by including only post
. Now, if we click on Sign Out
button, Post
is not being re-rendered:
This is because, our customComparator
checks for equality of incoming values of only post
and excludes signedIn
from the comparison.
Summary
In this post, we acknowledged what memoization is and why it is important in React. We learned about the use of React.memo()
, useMemo
and useCallback
APIs for implementing memoization in a React app.
By investigating a demo blog post app, we observed in the browser console that React.memo()
is very useful in preventing unnecessary, frequent re-renders of a component due to ancestor state changes that it does not depend on. A good example involves a component that accepts props whose values change often and/or on demand. With a React.memo
custom comparator function, we can choose to specify only the props we want to track for triggering a re-render of our component.
In the next article, we will turn our attention to the <Blog />
component and memoize a sorting function with useMemo()
hook.
Live Example
npm create refine-app@latest -- --example blog-react-memoization-memo