[React Basics] Understanding React.memo: When to Use It and When Not To?

You’ve probably heard the saying "don’t optimize prematurely." But in the world of React, where every unnecessary re-render can erode performance and user experience, mastering optimization tools isn’t "premature"—it’s essential. And React.memo is one of the most powerful weapons in your arsenal.

Understanding React.memo: When to Use It and When Not To?

This article will take you from the basics to advanced techniques, helping you master React.memo and turn it into a trusty sidekick for building lightning-fast React apps. ⚡

The Eternal Problem: Unnecessary Re-renders

To understand why React.memo is important, we first need to understand React’s default rendering behavior. When the state or props of a parent component change, React re-renders itself and all its child components, regardless of whether the child’s props have changed.

Consider this simple example:

import React, { useState } from 'react'

// Child component displaying user name
const UserProfile = ({ name }) => {
  console.log(`Rendering UserProfile with name: ${name}`)
  return <div>User name: {name}</div>
}

// Parent component
const App = () => {
  const [count, setCount] = useState(0)

  console.log('Rendering App...')

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me: {count}</button>
      <hr />
      {/* UserProfile is unrelated to "count" */}
      <UserProfile name="Alex" />
    </div>
  )
}

export default App

When you run this app and click the button, you’ll see in the console:

Rendering App...
Rendering UserProfile with name: Alex

Every click, App re-renders and so does UserProfile, even though its name prop hasn’t changed. In a small app, this is negligible. But imagine UserProfile is a complex component with heavy logic, and it sits in a huge component tree. These unnecessary re-renders can become a serious performance bottleneck.

This is where React.memo comes in and shines.

React.memo: The "Bodyguard" for Your Component

React.memo is a Higher-Order Component (HOC). Simply put, it’s a function that "wraps" a component and returns an optimized version.

Memoization is an optimization technique that caches the results of expensive computations and returns the cached result when the same input occurs again. React.memo applies this principle to component rendering.

The "memoized" version will only re-render when its props change.

Basic Usage

The syntax for React.memo is very simple. Just wrap your component in React.memo():

import React from 'react'

const MyComponent = (props) => {
  /* render logic */
}

// Export the memoized version
export default React.memo(MyComponent)

Now, let’s apply it to the UserProfile example above:

import React, { useState } from 'react'

// Child component wrapped with React.memo
const UserProfile = React.memo(({ name }) => {
  console.log(`Rendering UserProfile with name: ${name}`)
  return <div>User name: {name}</div>
})

// Parent component unchanged
const App = () => {
  const [count, setCount] = useState(0)
  console.log('Rendering App...')
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me: {count}</button>
      <hr />
      <UserProfile name="Alex" />
    </div>
  )
}

Now the result?

  • First render:
    Rendering App...
    Rendering UserProfile with name: Alex
    
  • Subsequent clicks:
    Rendering App...
    

UserProfile no longer re-renders! React.memo compares the previous props { name: 'Alex' } and the new props { name: 'Alex' }, sees they’re the same, and skips the re-render, reusing the previous output.

Shallow Comparison – The Achilles’ Heel

By default, React.memo performs a shallow comparison on the props object. This means it only compares the first level of props.

  • Works well with: Primitive types like string, number, boolean, null, undefined.
  • Problematic with: Reference types like object, array, and especially function.

Why is this a problem? Because when comparing reference types, JavaScript compares references (memory addresses), not the actual values inside.

Consider this classic example:

//...
const MemoizedButton = React.memo(({ onClick }) => {
  console.log('Rendering Button')
  return <button onClick={onClick}>Memoized Button</button>
})

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

  // The problem!
  // Every App re-render creates a NEW "log" function
  const logMessage = () => {
    console.log('Button clicked!')
  }

  return (
    <div>
      {/* ... */}
      <MemoizedButton onClick={logMessage} />
    </div>
  )
}

Even with React.memo, MemoizedButton will re-render every time App re-renders. Why? Because every time App runs, a brand new logMessage function is created. Even though the code is identical, their references in memory are different. React.memo compares prevProps.onClick !== nextProps.onClick and sees they’re different, so it allows a re-render.

This is where the "dynamic duo" of React.memo comes in.

The Perfect Pair: useCallback and useMemo

To solve the problem with reference types, we need to ensure we pass the same reference across renders (unless it truly needs to change).

useCallback: Memoize Your Functions

The useCallback hook was made to solve this exact problem with callback functions. It returns a "memoized" version of your callback, and this version only changes if one of its dependencies changes.

Let’s fix the example above with useCallback:

import React, { useState, useCallback } from 'react'

// ... MemoizedButton component

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

  // Use useCallback to memoize logMessage
  // Empty dependency array [] means this function is created only once
  const logMessage = useCallback(() => {
    console.log('Button clicked!')
  }, [])

  return (
    <div>
      {/* ... */}
      <MemoizedButton onClick={logMessage} />
    </div>
  )
}

Now, MemoizedButton will only re-render when logMessage actually changes (in this case, never, since the dependency array is empty). Problem solved!

useMemo: Memoize Your Values

Similar to useCallback, but useMemo is for memoizing a value (usually the result of an expensive computation, or an object/array).

import React, { useState, useMemo } from 'react'

const UserDetails = React.memo(({ user }) => {
  console.log('Rendering UserDetails')
  return (
    <div>
      {user.name} - {user.age}
    </div>
  )
})

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

  // Problem: Every re-render creates a NEW user object
  // const user = { name: 'Alex', age: 1 };

  // Solution: Use useMemo
  const user = useMemo(
    () => ({
      name: 'Alex',
      age: 1,
    }),
    [],
  )

  return (
    <div>
      {/* ... */}
      <UserDetails user={user} />
    </div>
  )
}

By combining React.memo with useCallback and useMemo, you can precisely control re-renders and prevent most unnecessary cases.

When Shallow Comparison Isn’t Enough: Custom Comparison Logic

In some complex cases, the default shallow comparison isn’t enough. React.memo gives you a "backdoor": an optional second argument—a comparison function.

React.memo(Component, areEqual(prevProps, nextProps))

The areEqual function receives the old and new props. It must return:

  • true: If the props are considered equal, the component will NOT re-render.
  • false: If the props are considered different, the component will re-render.

Note: This logic is the opposite of shouldComponentUpdate in class components (which returns false to skip re-render).

const UserCard = ({ user }) => {
  // ...
}

const areUserPropsEqual = (prevProps, nextProps) => {
  // Only re-render if user.id changes, ignore other changes
  return prevProps.user.id === nextProps.user.id
}

export default React.memo(UserCard, areUserPropsEqual)

This is a powerful tool, but use it carefully. A complex comparison function can cost more than just re-rendering the component.

Summary: When Should and Shouldn’t You Use React.memo?

React.memo isn’t a silver bullet. Wrapping every component in React.memo can backfire due to the cost of comparing props. Be a wise developer.

✅ Use React.memo when:

  1. Pure component: The component always returns the same output for the same input (props).
  2. Frequently rendered with the same props: The component is re-rendered often but its props rarely change.
  3. Heavy computation: The component’s render logic is complex and expensive, so skipping a re-render brings significant performance benefits.

❌ Think twice or avoid using React.memo when:

  1. Props almost always change: If the component’s props are almost always different between renders, the comparison is wasteful and only slows your app down.
  2. Component is too simple: For lightweight components (e.g., a div with a few attributes), the cost of re-rendering is negligible. Adding React.memo can complicate your code without clear benefit.

Golden advice: Always profile your app’s performance before and after optimizing. Tools like React DevTools Profiler will show you exactly which components are causing issues and whether React.memo is truly effective.

Good luck on your journey to mastering React performance!

Related Posts

[React Basics] State in React: Concepts, Usage, and Detailed Examples

Learn the concepts, declaration, and usage of State to manage dynamic data in your React application. See detailed guide with examples.

[React Basics] What is React? Why Should You Learn React Today?

Learn what React is and why it is one of the world’s most popular JavaScript libraries. Discover how React builds user interfaces quickly and efficiently.

[React Basics] Form Handling in React: Master State, Controlled Components, and Validation

Form handling in React is easier than ever. This article explains controlled components and how to build a complete form from start to finish.

[React Basics] Rendering Lists in React: Best Practices and Performance Optimization

Learn techniques for rendering lists in React, from basic .map() usage to advanced methods for handling dynamic data and optimizing performance. This guide is for all levels.