Skip to main content
Refine AI
React Memo Guide with Examples
19 min read

React Memo Guide with Examples

This article was last updated on September 19, 2024, to add sections on Deep vs Shallow Comparison in Memoization, Profiling Components in DevTools, and Best Practices for Using React.memo().

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?

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:

  1. React Memo Guide with Examples
  2. React useMemo Hook Guide With Examples
  3. 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:

  1. Clone this repository.
  2. Open it in your code editor and install the packages:
yarn install
  1. Then run the app:
yarn start
  1. Open Google Chrome and navigate to http://localhost:3000.
  2. Use CTRL + Shift + J on Ubuntu or Command + 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:

src/components/App.jsx
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:

src/components/Blog.jsx
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:

src/components/LatestPost.jsx
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:

src/components/Post.jsx
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:

memo1

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:

src/components/Post.jsx
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:

memo2

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 />:

memo3

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.

Best Practices for Using React.memo()

I wanted to share a few best practices for using React.memo() with some code examples. These can help improve the performance of our React components:

Use it for frequently re-rendered components

If a component is being re-rendered unnecessarily due to its parent re-rendering, React.memo() can help. For example, if we have a component like this:

const Post = ({ title, content }) => {
console.log("Rendering Post component");
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
};

export default Post;

We can prevent unnecessary re-renders by wrapping it with React.memo():

const Post = React.memo(({ title, content }) => {
console.log("Rendering Post component");
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
});

export default Post;

Now, this component will only re-render if its title or content props change.

Avoid overusing React.memo()

It’s important not to use React.memo() everywhere. If a component’s props change frequently, memoization can add more overhead than improvement. Use it only when the props don't change often.

Shallow comparison only

By default, React.memo() performs a shallow comparison of props. If you’re passing complex objects or arrays, you might need to write a custom comparison function. For example:

const Post = React.memo(
({ title, content }) => {
console.log("Rendering Post component");
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
},
(prevProps, nextProps) => {
return prevProps.title === nextProps.title; // Custom comparison
},
);

In this example, we are only checking if the title has changed. The component will only re-render if the title prop changes, even if content changes.

Don’t memoize static or rarely updated components

If a component is static or doesn’t receive changing props, there’s no need to use React.memo(). For example, this component doesn’t benefit from memoization:

const Footer = () => {
return <footer>Footer content</footer>;
};

// No need to memoize here

Memoization would add unnecessary complexity without improving performance.

By following these best practices and using React.memo() in the right situations, we can optimize performance without adding extra overhead.

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:

src/components/Post.jsx
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:

memo4

This is because, our customComparator checks for equality of incoming values of only post and excludes signedIn from the comparison.

You can Profile Components in DevTools

I wanted to share some tips on Profiling Components in DevTools to help us identify and fix performance bottlenecks in our React app.

Opening the React Profiler

  • Install the React Developer Tools extension for Chrome or Firefox.
  • Open DevTools (F12 or Ctrl + Shift + I), go to the Profiler tab, and click "Record" before interacting with the app.

Capturing Component Renders

Interact with the app while recording (e.g., clicking buttons, changing state). The Profiler will track which components re-render and how long each takes.

Analyzing Results

After recording, view the timeline to inspect:

  • Render Time: Time taken for each component render.
  • Render Reason: Prop or state changes causing re-renders.

Optimizing Slow Renders

If a component (e.g., Post) is re-rendering unnecessarily, wrap it in React.memo() to prevent it from re-rendering when props haven't changed.

Highlight Updates

Enable "Highlight Updates" in the React tab to visually see components that re-render, making it easier to spot unnecessary updates.

Using the Profiler, we can quickly identify and optimize slow re-renders.

Bonus: Deep vs Shallow Comparison in Memoization

I wanted to go over deep vs shallow comparison in memoization and how that affects performance optimization in React, especially when using React.memo() and other memoization techniques.

Shallow Comparison

By default, React does shallow comparison to check if a component’s props have changed to decide whether to re-render it. Shallow comparison means React checks for changes only at the top level of an object or array and doesn't go into nested properties.

For example:

const person1 = { name: "John" };
const person2 = { name: "John" };

console.log(person1 === person2); // false - because of different object references

Even though person1 and person2 have the same data, during a shallow comparison, they are considered different because the reference is compared, not the content.

const Post = React.memo(({ title, content }) => {
console.log("Rendering Post component");
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
});

If the title and content props are primitives (like strings or numbers), the shallow comparison works as expected. But if they are objects or arrays, even a slight change in the reference (e.g., creating a new object) will trigger a re-render.

Deep Comparison

A deep comparison goes beyond top-level properties and checks all nested properties. It's a bit heavier since every level of the object or array has to be checked.

React doesn't perform deep comparisons by default in React.memo() because it can be slow, especially with deeply nested objects or large arrays.

Custom Comparators for Deep Comparison

If shallow comparison isn’t enough (for example, when passing complex objects as props), we can provide a custom comparator function in React.memo() to implement deep comparison.

Here’s a custom comparator for deep comparison:

import React from "react";
import { isEqual } from "lodash";

const Post = ({ post }) => {
console.log("Rendering Post component");
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
};

const customComparator = (prevProps, nextProps) => {
return isEqual(prevProps.post, nextProps.post); // Deep comparison using lodash
};

export default React.memo(Post, customComparator);

Here, we’re using lodash's isEqual() function to deep compare the entire post object. This helps avoid unnecessary re-renders when only the reference changes, but the data inside remains the same.

When to Use Deep Comparison

  • Complex Data Structures: When passing large or deeply nested objects as props, and you don’t want to re-render components unnecessarily.
  • Performance Trade-offs: Deep comparison might be slower than a shallow comparison, so it’s important to measure and ensure that the performance gain from avoiding re-renders outweighs the cost of deep comparison.

Best Practices

  • Use shallow comparison whenever possible to keep performance high.
  • Only use deep comparison when you’re sure the props involve deeply nested objects that don’t change often.
  • Use libraries like lodash or deep-equal for effective deep comparisons.

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

Run on your local
npm create refine-app@latest -- --example blog-react-memoization-memo