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.

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.memoapplies 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 especiallyfunction.
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:
- Pure component: The component always returns the same output for the same input (props).
- Frequently rendered with the same props: The component is re-rendered often but its props rarely change.
- 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:
- 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.
- Component is too simple: For lightweight components (e.g., a
divwith a few attributes), the cost of re-rendering is negligible. AddingReact.memocan 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!
![[React Basics] What is React? Why Should You Learn React Today?](/images/blog/what-is-react.webp)
![[React Basics] Dynamic Routes in React: The Secret to Building Flexible Apps](/images/blog/dynamic-routes-in-react.webp)
![[React Basics] Form Handling in React: Master State, Controlled Components, and Validation](/images/blog/form-handling-in-react.webp)