Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 67

Optimizing React Components with React.memo

Recommended resources

Introduction

In our previous lesson, we established a crucial rule of React rendering: by default, a parent component's re-render triggers a re-render of all its children. We then learned how to use the React DevTools Profiler to spot these "wasted" renders—instances where a child re-renders even though its output would be identical.

Today, we'll focus on the primary tool React provides to prevent these unnecessary renders. This lesson directly addresses the learning outcome: Explain how React.memo functions as a higher-order component for preventing re-renders.

We will explore what React.memo is, how its core comparison mechanism works, and, most importantly for your goal of understanding the "why," we'll investigate the common pitfalls related to JavaScript's referential identity that can break this optimization. We'll even take a brief look at how React handles memoized components internally during its reconciliation phase.


1. What is React.memo?

React.memo is a Higher-Order Component (HOC). An HOC is a function that takes a component and returns a new, enhanced component. In this case, React.memo returns a new component that has a special behavior: it "memoizes" the rendered output.

This means if the component is rendered with the same props as the last time it rendered, React will skip the re-render entirely and reuse the previous result.

This video provides a clear, practical demonstration of applying React.memo and observing its effect.

Mastering React Memo

The 'Mastering React Memo' video by Jack Herrington will show us the basic application of React.memo and its immediate impact on rendering.

Watch the section from 00:03:03 to 00:06:04. Pay attention to how wrapping the Swatch component in memo prevents the 'swatch rendered' message from being logged on every parent re-render, but still allows it to render when its color prop actually changes.

This simple optimization is a direct solution to the "wasted render" problem we diagnosed in the last lesson.

React.memo in Action: Preventing Re-renders with DevTools Profiler
This image from the React DevTools Profiler shows a component, `CharacterMap (Memo)`, that 'Did not render.' This is the outcome we aim for with `React.memo`: React has successfully determined that the component's props did not change and bailed out of the render, saving computational effort.

2. The Core Mechanism: Shallow Prop Comparison

You might wonder how React.memo decides if the props are "the same." By default, it performs a shallow comparison of the props object. It iterates over the keys of the old and new props objects and compares the values using Object.is() (which is very similar to the === strict equality check).

  • For primitive props (strings, numbers, booleans, null, undefined), this works as you'd expect. 5 === 5 is true.
  • For reference-type props (objects, arrays, functions), the comparison checks if the references point to the exact same object in memory, not if their contents are equivalent. {} === {} is false.

This distinction is the single most important concept to grasp when working with React.memo.

React Memoization vs. Classical Memoization

It's also important to clarify that React.memo's behavior is different from the classical computer science definition of memoization, which typically involves caching results for many different inputs. React.memo only compares the current props to the previous props. It does not maintain a large cache of past results.

Mastering React Memo

Let's return to the 'Mastering React Memo' video for an excellent explanation of this distinction.

Watch the segment from 00:06:04 to 00:12:18. The video contrasts React's memoization with a traditional memoization implementation from Lodash, building a conceptual model of React's 'previous vs. new' props comparison. This directly addresses your interest in the underlying mechanics.


3. The Pitfall: Referential Identity

The reliance on shallow comparison and referential identity is where React.memo becomes tricky. As we saw in the last lesson, it's very common to define objects, arrays, or functions inside a component's render body.

function App() {
  const [count, setCount] = useState(0);

  // A new object and a new function are created on every render of App
  const userStyle = { color: 'blue', fontSize: 16 };
  const handleClick = () => console.log('Clicked!');

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Re-render App</button>
      {/* MemoizedComponent will re-render every time,
          because `userStyle` and `handleClick` are new references. */}
      <MemoizedComponent style={userStyle} onClick={handleClick} />
    </div>
  );
}

Every time App re-renders, userStyle and handleClick are new objects in memory. The shallow comparison inside MemoizedComponent will see prevProps.style !== newProps.style and prevProps.onClick !== newProps.onClick, and it will re-render, completely negating the optimization.

The following resources demonstrate this problem and introduce the solution: using the useMemo and useCallback hooks to preserve referential identity across renders.

Mastering React Memo

This next segment from 'Mastering React Memo' shows exactly how passing an object literal as a prop breaks memoization.

Watch from 00:17:45 to 00:19:51. Note how passing an object for params causes the memoized component to re-render every time.

Blogged Answers: A (Mostly) Complete Guide to React Rendering ...

The blog post 'A (Mostly) Complete Guide to React Rendering Behavior' provides a great textual explanation of this exact problem and the hooks that solve it.

Please read the sections 'How New Props References Affect Render Optimizations' and 'Optimizing Props References'. This reinforces the video content and explicitly connects React.memo to useMemo and useCallback as necessary companions.

To complete the picture, let's see how useMemo and useCallback are used to fix the problem.

Mastering React Memo

Finally, let's see how useMemo and useCallback are used to provide stable references for object and function props, making React.memo effective.

Watch the final two key segments: 00:19:51 - 00:23:38: This part shows how to wrap the object prop in useMemo to preserve its identity. 00:23:38 - 00:25:28: This demonstrates using useCallback to do the same for a function prop (onClick).

The key takeaway is that React.memo is often not a standalone solution. It requires a disciplined approach to managing prop references in the parent component.


4. Under the Hood: React's Internal Handling

Given your interest in how things work internally, let's briefly look at the React source code's logic. You don't need to read the code itself, but understanding the high-level process is insightful.

When you write const MyMemoizedComponent = React.memo(MyComponent);, the memo function doesn't return a standard component. It returns a special object with a property $$typeof: REACT_MEMO_TYPE.

During reconciliation, when React's beginWork phase encounters a fiber node corresponding to this element, it follows a specific path:

  1. It identifies the element by its special $$typeof.
  2. It calls an internal function named updateMemoComponent.
  3. This function retrieves the previous and next props.
  4. It uses a comparison function (either one you provide as a second argument to memo, or it defaults to shallowEqual).
  5. If the comparison returns true (meaning props are equal), it calls bailoutOnAlreadyFinishedWork, which stops the render process for this subtree.
  6. If the comparison returns false, the render proceeds as normal.

How does React.memo() work internally? - JSer.dev

The article 'How does React.memo() work internally?' provides a fantastic summary of this process by walking through the relevant parts of the React source code.

Skim through the sections '"React.memo()" creates a new element: REACT_MEMO_TYPE', 'reconciliation of MemoComponent', and '"updateMemoComponent()"'. You don't need to understand every line of code. Focus on the high-level concepts I've just outlined: the creation of a REACT_MEMO_TYPE element and how that leads to the updateMemoComponent function, which performs the comparison and decides whether to 'bailout'.

This internal path is why React.memo is more than just a simple wrapper; it's a hook into React's core reconciliation algorithm.


5. Strategy: When to Use React.memo

React.memo is an optimization, and like any optimization, it has a cost—the prop comparison itself. It should be used strategically, not universally. The debate on "should I memoize everything?" is a common one.

Blogged Answers: A (Mostly) Complete Guide to React Rendering ...

Let's revisit Mark Erikson's article for some expert perspective on this strategic question.

Read the section 'Memoize Everything?'. It includes insights from React core team member Dan Abramov and provides a balanced view. The key is to use memo when a component is expensive to render and often receives the same props.

In short, apply React.memo when you have profiled your application (as we learned in the last lesson) and identified a component that is:

  1. Rendering unnecessarily.
  2. Expensive enough that the cost of re-rendering is greater than the cost of the prop comparison.

Conclusion

In this lesson, we took a deep dive into React.memo, moving from its basic application to its internal mechanics and strategic use.

Key Takeaways:

  • HOC for Optimization: React.memo is a Higher-Order Component that prevents a component from re-rendering if its props have not changed.
  • Shallow Comparison is Key: It works by performing a shallow comparison of props. This is fast but sensitive to referential identity.
  • The Referential Identity Trap: Passing new object, array, or function references as props will break memoization.
  • The Solution: useMemo and useCallback are essential tools to provide stable references for props passed to a memoized component.
  • Internal Mechanism: React.memo signals to the reconciler to use a special update path that includes a "bailout" check before rendering.
  • Strategic Application: It's a performance tool to be used deliberately on expensive components, not a default for every component.

What's Next?

We have now covered optimizations that happen during the render phase of your application. But what about the very first time a user loads your page? If your application is large, sending all the JavaScript at once can lead to slow initial load times. In our next lesson, we will explore a strategy for tackling this: code-splitting using React.lazy and Suspense, which allows us to defer loading component code until it's actually needed.