One of the benefits of using React is its improved performance, which allows your web applications to load faster and allows you to navigate from one page to another without waiting a lot. There are scenarios where we can further improve React’s native performance, and in this article, we will see how to use Memo
to improve performance.
React Memo allows us to memoize our component code and avoid unnecessary re-renders when the same props are passed to our components, thereby enhancing the performance of our React application.
What is Memoization?
#What is React Memo?
Components in React are designed to re-render whenever the state or props value changes. Also, when a parent component re-renders, so do all of its child components. This can impact our application performance because, even if the change is only intended to affect the parent component, all child components attached to the parent component will be re-rendered. Ideally, child components should only re-render if their state or the props passed to them change.
React Memo is a higher-order component that wraps around a component to memoize the rendered output and avoid unnecessary renderings in cases where the props passed to the child component are the same. This improves performance because it memoizes the result and skips rendering to reuse the last rendered result.
There are two ways to use memo()
:
We can wrap the actual component directly using memo:
import { memo } from "react";const myComponent = memo((props) => {/* component code */});export default myComponent;
Another option is to create a new variable to store the memoized component and then export the new variable:
import { memo } from "react";const myComponent = (props) => {/* render using props */};export const MemoizedComponent = memo(myComponent);
In the example above, myComponent
outputs the same content as MemoizedComponent
, but the difference between both is that MemoizedComponent’s render is memoized
. This means that this component will only re-render when the props
change.
Pro Tip
#How to use React Memo
Let us understand React Memo with a Todo List example. We have a simple React-Typescript index.tsx
component here.
index.tsx
import { useState } from "react";import { ITodo } from "./types";import Todo from "./Todo";export function App() {console.log("App component rendered");const [todo, setTodo] = useState<ITodo[]>([{ id: 1, title: "Read Book" },{ id: 2, title: "Fix Bug" },]);const [text, setText] = useState("");const addTodo = () => {const lastId = todo[todo.length - 1].id;let newTodo = { id: lastId + 1, title: text };setTodo([...todo, newTodo]);setText("");};return (<div><inputtype="text"value={text}onChange={(e) => setText(e.target.value)}/><button type="button" onClick={addTodo}>Add todo</button><Todo list={todo} /></div>);}export default Memo;
types.ts
export interface ITodo {id: number;title: string;}
In the component above, we are using state to hold all the todo items in an array, there is an input for adding a new todo to the array. Notice that at the beginning of the component, a console.log() statement will be executed when our component is rendered. The Todo
component is a child component of the App
component and we are passing the todo item list as a prop to it.
Todo.tsx
import { memo } from "react";import { ITodo } from "./types";import TodoItem from "./TodoItem";interface TodoProps {list: ITodo[];}const Todo = ({ list }: TodoProps) => {console.log("Todo component rendered");return (<ul>{list.map((item) => (<TodoItem key={item.id} item={item} />))}</ul>);};export default Todo;
In the code above, a console.log() statement will log a text to show when the Todo component renders. The Todo
component receives the todo item list, iterates over it, and passes each todo item as a prop further to a TodoItem
component.
TodoItem.tsx
import { memo } from "react";import { ITodo } from "./types";interface TodoItemProps {item: ITodo;}const TodoItem = ({ item }: TodoItemProps) => {console.log("TodoItem component rendered");return <li>{item.title}</li>;};export default TodoItem;
The TodoItem
component above also has a console.log statement to help us understand when it renders. When we run our application and check the console, we can notice that all three components render.
We see four logs, one for the parent component (App.js), one for the Todo component, and finally, the TodoItem component renders twice because the initial todo list contains two elements. This is normal for the initial render.
Now, when we change something in the parent component that doesn’t affect the child components, only the parent component should be re-rendered. For example, when we type anything in the text field, the state of App component changes and it re-renders. This leads to unnecessary renders of the child Todo and TodoList components, even though typing in the App component should not affect the children components.
Optimizing components with React Memo
Let us now memoize some of the children components so that they only render when there is a change in props. The first component that would be memoized is the Todo
component. It is important to stop re-renders whenever a user types in the text field.
We can do this by wrapping the Todo component with memo()
Todo.tsx
// Memoized Todo componentimport { memo } from "react";import { ITodo } from "./types";import TodoItem from "./TodoItem";interface TodoProps {list: ITodo[];}const Todo = memo(({ list }: TodoProps) => {console.log("Todo component rendered");return (<ul>{list.map((item) => (<TodoItem key={item.id} item={item} />))}</ul>);});export default Todo;
Now that the Todo component is memoized, only the App
component will re-render whenever its state changes. The Todo
component will only re-render when the list prop passed to it changes. Let us now take this a step further to avoid unnecessary re-rendering whenever an item is added to the todo array.
When the todo state changes, it affects the list prop, and all existing TodoItem
components on the screen will render for each item added. We want a situation where only the new item added be rendered not the existing ones. We can achieve this by memoizing the TodoItem
component:
TodoItem.tsx
// Memoized TodoItem componentimport { memo } from "react";import { ITodo } from "./types";interface TodoItemProps {item: ITodo;}const TodoItem = memo(({ item }: TodoItemProps) => {console.log("TodoItem component rendered");return <li>{item.title}</li>;});export default TodoItem;
How to use custom comparison function with React Memo
React Memo makes a shallow comparison and might not function as you wish in some scenarios. If we want to have control over the comparison, we can provide a custom comparison function as the second argument.
For example, if we are passing an object containing user details as a prop to a Profile
component:
index.tsx
import { useState } from "react";import { IUser } from "./types";const App = () => {console.log("App rendered");const [text, setText] = useState("");let user: IUser = { name: "John Doe", age: 23, username: "johndoe" };return (<div><inputtype="text"value={text}onChange={(e) => setText(e.target.value)}/><Profile user={user} /></div>);};
types.tsx
export interface IUser {name: string;age: number;username: string;}
Profile.tsx
import { memo } from "react";import { IUser } from "./types";interface ProfileProps {user: IUser;}const Profile = memo(({ user }: ProfileProps) => {console.log("Profile rendered");return (<div><p>{user.name}</p><p>{user.age}</p><p>{user.username}</p></div>);});export default Profile;
The memoized Profile component will always render even when the user object does not change. Ideally, in this case, we should use useMemo
in the parent component to fix the object passed to the Profile component. However, to understand a custom comparison function, we will implement that approach for now.
React Memo doesn't work because it only performs a shallow comparison of the component's properties. Every time the app is updated, the user variable is re-declared. We can use the second argument of the memo and provide a custom comparison function.
Profile.tsx
import { memo } from "react";import { IUser } from "./types";interface ProfileProps {user: IUser;}const arePropsEqual = (prevProps: ProfileProps, nextProps: ProfileProps) => {if (prevProps.user.name === nextProps.user.name) {return true; // props are equal}return false; // props are not equal -> update the component};const Profile = memo(({ user }: ProfileProps) => {console.log("Profile rendered");return (<div><p>{user.name}</p><p>{user.age}</p><p>{user.username}</p></div>);}, arePropsEqual);export default Profile;
#When to use React Memo
We now understand what it means to memoize a component and the advantages of optimization. This doesn’t mean that we should memoize all our components to ensure maximum performance optimization of performance 🙃.
It is important to know when and where to memoize your component else it will not fulfill its purpose.
For example, React Memo is used to avoid unnecessary re-renders due to the same props being passed but if the state and content of your component will ALWAYS change, React Memo becomes useless.
Also, when we need to remember the values of a function or an object, we should hooks like useMemo()
and useCallback()
.
Here are points when we should consider using memo:
- The component is medium to big size (contains a decent amount of UI elements) to have props equality check, not very simple and small. We don’t want to optimize where optimization is more costly than re-rendering.
- Component renders quite often.
- Component renders with the same props often and its state/context doesn’t change quite often.
Finally, please note that memo is just a performance optimization. We should NOT rely on it for our component logic to work properly.
#Wrapping Up
In this article, we have understood what React Memo is and its usage, why, and when to use React Memo. We have also learned that using React Memo correctly prevents unnecessary re-renderings when the next props are equal to the previous ones.
Have fun coding!
Blog Authors