DEV Community

Mohamed Idris
Mohamed Idris

Posted on

Optimizing Re-Renders Using React.memo()

In React, unnecessary re-renders can occur when the parent component re-renders, even if the child component's props haven't changed. This can lead to performance issues, especially in complex applications. One way to prevent this is by using React.memo(), which helps memoize a component. This means React will only re-render the component if its props have changed.

In this example, let’s assume we can't move the count state to a separate component because it's needed in the parent App component. However, we can still optimize the List component by using React.memo() so it only re-renders when the people prop changes.

Before Using React.memo()

Without optimization, every time the count state changes in the parent App component, the App re-renders. This also causes the List component to re-render, even though the people prop passed to List hasn't changed. This is inefficient.

App.jsx (Before Using React.memo()):

import { useState } from 'react';
import { data } from '../../../../data';
import List from './List';

const App = () => {
  const [people, setPeople] = useState(data);
  const [count, setCount] = useState(0);

  return (
    <section>
      <button
        className='btn'
        onClick={() => setCount(count + 1)}
        style={{ marginBottom: '1rem' }}
      >
        count {count}
      </button>
      <List people={people} />
    </section>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode
  • Problem: Every time the count state changes, the App component re-renders. This also causes the List component to re-render, even though the people prop hasn't changed.

Visual Example:
Before: List re-renders unnecessarily when count changes.
Before

After Using React.memo()

To optimize this, we can wrap the List component with React.memo(). This will ensure that List only re-renders when its people prop changes, not when the count state changes.

List.jsx (After Using React.memo()):

import { memo } from 'react';
import Person from './Person';

const List = ({ people }) => {
  return (
    <div>
      {people.map((person) => {
        return <Person key={person.id} {...person} />;
      })}
    </div>
  );
};

// Memoize the List component
export default memo(List);
Enter fullscreen mode Exit fullscreen mode
  • How it works:

    • React.memo() compares the people prop of the List component. If people hasn’t changed, React skips re-rendering List.
    • List will only re-render when the people prop changes, even if the parent App component re-renders due to the count state change.

Visual Example:
After: List re-renders only when the people prop changes, not when count changes.
After

Performance Improvement

By using React.memo(), we prevent unnecessary re-renders of the List component when the count state in the parent App component changes. This leads to a more efficient rendering process, especially in applications with large datasets or complex UIs.


Summary

  • React.memo() is a higher-order component that helps prevent unnecessary re-renders by memoizing the component.
  • In this example, we wrapped the List component with React.memo() so it only re-renders when the people prop changes, not when the parent App component's state changes.
  • This optimization is useful for improving performance, particularly when the parent component frequently re-renders but the child component's props remain unchanged.

Top comments (1)

Collapse
 
edriso profile image
Mohamed Idris

MIND GRENADE!!! 💣

Imagine we need to add a removePerson method to remove a person from a list. So, let’s start with the basic setup in App.jsx:

App.jsx

import { useState } from 'react';
import { data } from '../../../../data';
import List from './List';

const App = () => {
  const [people, setPeople] = useState(data);
  const [count, setCount] = useState(0);

  const removePerson = (id) => {
    const newPeople = people.filter((person) => person.id !== id);
    setPeople(newPeople);
  };

  return (
    <section>
      <button
        className="btn"
        onClick={() => setCount(count + 1)}
        style={{ marginBottom: '1rem' }}
      >
        count {count}
      </button>
      <List people={people} removePerson={removePerson} />
    </section>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode

In this code, we’re removing a person by filtering the people array, and passing the removePerson function down to the List component.

List.jsx (Memoized Component)

import { memo } from 'react';
import Person from './Person';

const List = ({ people, removePerson }) => {
  return (
    <div>
      {people.map((person) => {
        return (
          <Person
            key={person.id}
            {...person}
            removePerson={() => removePerson(person.id)}
          />
        );
      })}
    </div>
  );
};
export default memo(List);
Enter fullscreen mode Exit fullscreen mode

We use memo here to memoize the List component, so it only re-renders if the people or removePerson props change.

Person.jsx (Displaying Each Person)

const Person = ({ name, removePerson }) => {
  return (
    <div>
      <h4 onClick={removePerson}>{name}</h4>
    </div>
  );
};
export default Person;
Enter fullscreen mode Exit fullscreen mode

What’s the Problem?

Now, everything works fine when we remove a person—List re-renders because the people array changes. But what happens when we click the count button? 🤔

The Unexpected Re-render!

When we click on the count button, the removePerson function is re-created on each render. Because the function reference changes, it’s considered a new value by React. So, even though the people array hasn’t changed, the removePerson function prop is treated as "changed," causing the List component to re-render unnecessarily.

The Memoization Breakdown

In the List component, we used memo, which only prevents re-renders if the props change. However, the function passed as a prop (removePerson) is created from scratch every time the App component re-renders (because of the setCount state change). This means that React sees the removePerson function as "changed" every time the App re-renders.

Here’s what happens:

  • Expected behavior: List should not re-render if only the count changes and the people array stays the same.
  • Actual behavior: List does re-render because the removePerson function is recreated every time the App component re-renders.

DevTools Insight

We can see this in the React DevTools Profiler. Even though the people array hasn’t changed, we can clearly see that the removePerson function is treated as a prop change, triggering a re-render of the List component.

Profiler Example 1.1

Profiler Example 1.2

Here’s the breakdown:

  • The removePerson function is created from scratch on every render, and React treats it as a new prop.
  • This causes List to re-render, even though the people prop hasn’t changed.

Why Is This Happening?

  • memo is working fine for checking props, but it doesn’t handle functions that are recreated on each render.
  • React sees the function as a new value every time, causing unnecessary re-renders.

The Solution: useCallback

Next, we’ll introduce the useCallback hook, which will help us preserve the function reference across renders, preventing unnecessary re-renders in this scenario.

With useCallback, we can ensure that removePerson only gets recreated when the dependencies (like setPeople) change, not every time the component re-renders.

Stay tuned for the fix! 🔧


TL;DR:

  • Memoization works for props, but it doesn't prevent re-renders caused by function references changing.
  • Using useCallback will help us preserve the function reference and avoid unnecessary re-renders when only other state (like count) changes.