1. Memoizing Component with memo

In order to understand difference between memo, useMemo and useCallback, we will take example of todo-list.

Initial Setup

you can clone this codesandbox and skip to the problem in our app or create your own create-react-app with typescript and copy following files to it.

Note:

For anyone who is using react 18 versions, take a look at this before moving forward.

let's start with types.ts file, which just contains all the types which are used by multiple file in our app.

// types.ts

export type Todo = {
  id: number,
  task: string,
};

Task.tsx file contains Task component which just renders each task in our todo app.

// Task.tsx
import { useEffect } from "react";
import { Todo } from "./types";

export default function Task({ id, task }: Todo) {
  useEffect(() => {
    console.log("Rendering <Task />", task);
  });

  return <li>{task}</li>;
}

TodoList.tsx file contains TodoList component which renders a list of tasks with the help of the Task component.

// TodoList.tsx

import { useEffect } from "react";
import { Todo } from "./types";
import Task from "./Task";

export type Props = {
  todoList: Array<Todo>,
};

export default function TodoList({ todoList }: Props) {
  useEffect(() => {
    console.log("Rendering <List />");
  });

  return (
    <ul>
      {todoList.length ? (
        todoList.map(({ id, task }: Todo) => {
          return <Task key={id} id={id} task={task} />;
        })
      ) : (
        <h1>There are no tasks in the todo list</h1>
      )}
    </ul>
  );
}

At last we have App.tsx file we creates task and renders TodoList component with some initial tasks predefined and function to create a new task.

// App.tsx

import { useState, useEffect } from "react";
import { Todo } from "./types";
import TodoList from "./TodoList";

const initialTodos = [
  { id: 1, task: "Complete i3 setup dotfiles" },
  { id: 2, task: "Fix a bug in a project" },
];

export default function App() {
  const [todoList, setTodoList] = useState <Array<Todo>>(initialTodos);
  const [task, setTask] = useState("");

  useEffect(() => {
    console.log("Rendering <App />");
  });

  const handleCreate = () => {
    const newTodo: Todo = { id: Date.now(), task };

    setTodoList([...todoList, newTodo]);

    setTask("");
  };

  return (
    <>
      <input
        type="text"
        value={task}
        onChange={(e) => setTask(e.target.value)}
      />

      <button onClick={handleCreate}>Create</button>

      <TodoList todoList={todoList} />
    </>
  );
}

Problem in our app

All Three Components App, TodoList and Task contains a useEffect without dependency array which keep track of the their renders.

After the initial setup, you should be able to see the following output on the console.

Console Output

Now, if we try to write a new task in the input, we can see that for each letter we write, those renders appear again for each letter. by just typing "fo", we have three two more batches of renders.

Extra Renders Output

so we can clearly determine that App Component does not have good performance, this is where memo can help use to improve performance. in the next sections we're going to learn how to implement memo, useMemo, useCallback to memoize a component, a value and a function.

Memoizing a Component with memo

memo is a higher order component similar to PureComponent of React because it performs shallow comparison of props because of this if we pass same props all the the time, component is render once and will memoized. only way to re-render a component is when the prop changes its value.

As you might already figured out, this can be a possible solution for additional re-rendering of our TodoList and Task components.

To add memo, we just need to import it from react and pass the component that we want to memoized.

// TodoList.tsx
import { useEffect, memo } from "react";

...

export default memo(TodoList);
// Task.tsx
import { useEffect, memo } from "react";

...

export default memo(Task);

now, if we write 'fo' again in the input, we get following

fo after memo

Now, we get first batch of renders and 2 extra renders of App component, which is totally fine because we typed 'fo'.

Also, try creating a new task, here we're adding "complete assignment" as a new task and we got following.

after_memo.png

Only App component rendered 20 times(1 initial + 19 characters of "complete assignment" including space) because we have useState for creating new task in App component.

With this you might wonder that we should use memo with every component?, answer is no because of the performance, if you use memo everytime then a process of shallow comparison and memoization have inferior performance compared to one if we don't use it.

rule of thumb for using memo is just don't use it, unless you're working with large amount of data from API or your component needs to perform a lot of renders(e.g in a huge list) or your app is going slow.

Source:

for reference, here is the code we have so far.