React.memo Explained — Prevent Unnecessary Re-renders
In this tutorial, you will learn about React.memo Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
React.memo is a higher-order component that prevents re-rendering when a component's props have not changed, optimizing performance for expensive render operations.
What You'll Learn
- What React.memo does and when to use it
- How to customize prop comparison
- How React.memo differs from useMemo and useCallback
- When NOT to use React.memo
- How to measure memoization benefits
Why It Matters
Without memoization, React re-renders all child components whenever a parent re-renders. For large lists or expensive components, this causes performance issues. React.memo skips re-renders when props are unchanged.
Real-World Use
Durga Antivirus Pro's threat list renders hundreds of threat items. React.memo on each ThreatRow component prevents re-rendering all rows when only one row's data changes.
flowchart TD
A[Parent Re-renders] --> B{React.memo?}
B -->|Yes + Props Changed| C[Re-render Child]
B -->|Yes + Props Same| D[Skip Child]
B -->|No| C
style A fill:#3b82f6,color:#fff
Basic React.memo
Wrap a component with React.memo:
import { memo, useState } from "react";
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
console.log("ExpensiveList rendered");
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name} - {item.description}
</li>
))}
</ul>
);
});
function App() {
const [count, setCount] = useState(0);
const [items] = useState([
{ id: 1, name: "Item 1", description: "Description for item 1" },
{ id: 2, name: "Item 2", description: "Description for item 2" },
]);
const handleSelect = (id) => {
console.log("Selected:", id);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ExpensiveList items={items} onSelect={handleSelect} />
</div>
);
}
Expected output: Clicking "Increment" does NOT log "ExpensiveList rendered" because items and onSelect have not changed.
React.memo performs a shallow comparison of props. If all props are the same reference, the component skips rendering. The function component inside memo only runs when props actually change.
Custom Comparison Function
React.memo accepts a comparison function:
import { memo, useState } from "react";
const UserCard = memo(function UserCard({ user, onFollow }) {
console.log("UserCard rendered:", user.name);
return (
<div className="card">
<h3>{user.name}</h3>
<p>{user.bio}</p>
<button onClick={() => onFollow(user.id)}>
{user.isFollowed ? "Unfollow" : "Follow"}
</button>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if these specific fields changed
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.isFollowed === nextProps.user.isFollowed &&
prevProps.user.name === nextProps.user.name
);
});
function UserList() {
const [users, setUsers] = useState([
{ id: 1, name: "Alice", bio: "Developer", isFollowed: false, lastSeen: "2026-06-28" },
{ id: 2, name: "Bob", bio: "Designer", isFollowed: true, lastSeen: "2026-06-27" },
]);
const toggleFollow = (id) => {
setUsers(prev => prev.map(u =>
u.id === id ? { ...u, isFollowed: !u.isFollowed } : u
));
};
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} onFollow={toggleFollow} />
))}
</div>
);
}
Expected output: When only lastSeen property changes, UserCard does NOT re-render because the custom comparator ignores that field.
The comparison function receives previous and next props. Return true to skip rendering (props are equal), false to re-render. Be careful: returning the wrong value causes skipped renders or missed updates.
When React.memo Does Not Help
React.memo is not always beneficial:
import { memo, useState } from "react";
// BAD: Every render creates new objects
const Item = memo(function Item({ item, onAction }) {
console.log("Item rendered:", item.id);
return <div onClick={() => onAction(item.id)}>{item.name}</div>;
});
function BadList() {
const [items, setItems] = useState([{ id: 1, name: "A" }]);
const [count, setCount] = useState(0);
// Every render creates a new object -> memo fails
const handleAction = (id) => {
console.log("Action:", id);
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{items.map(item => (
<Item key={item.id}
item={item}
onAction={handleAction} // New function every render!
/>
))}
</div>
);
}
Expected output: The Item component re-renders every time because onAction is a new function reference on each parent render.
React.memo uses shallow comparison. If you pass inline functions, new objects, or new arrays, memo cannot prevent re-renders because each render creates new references.
React.memo with useCallback
Pair React.memo with useCallback to stabilize function references:
import { memo, useState, useCallback } from "react";
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log("TodoItem rendered:", todo.id);
return (
<li>
<input type="checkbox" checked={todo.completed}
onChange={() => onToggle(todo.id)} />
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>X</button>
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Build an app", completed: false },
]);
const [filter, setFilter] = useState("all");
// useCallback stabilizes function references
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, []);
const filtered = todos.filter(t => {
if (filter === "active") return !t.completed;
if (filter === "completed") return t.completed;
return true;
});
return (
<div>
<button onClick={() => setFilter("all")}>All</button>
<button onClick={() => setFilter("active")}>Active</button>
<button onClick={() => setFilter("completed")}>Completed</button>
<ul>
{filtered.map(todo => (
<TodoItem key={todo.id} todo={todo}
onToggle={handleToggle} onDelete={handleDelete} />
))}
</ul>
</div>
);
}
Expected output: Changing the filter does NOT re-render TodoItem components. Only the filtered list changes.
useCallback returns the same function reference as long as its dependencies do not change. Combined with React.memo, this prevents child re-renders when only the parent state changes in unrelated ways.
Measuring Memo Benefits
Measure before and after adding memo:
import { memo, useRef, useEffect, Profiler } from "react";
const MemoizedComponent = memo(function MemoizedComponent({ data }) {
return <div>{data.map(d => <span key={d}>{d}</span>)}</div>
});
function App() {
const renderCount = useRef(0);
const onRender = (id, phase, actualDuration) => {
console.log(`Render ${++renderCount.current}: ${phase} took ${actualDuration}ms`);
};
return (
<Profiler id="App" onRender={onRender}>
<MemoizedComponent data={["a", "b", "c"]} />
</Profiler>
);
}
Expected output: The Profiler logs render times. Compare with and without React.memo to measure the savings.
Use the Profiler API or React DevTools profiler to measure render performance. Only add memoization where it measurably improves performance.
Common Mistakes
Using React.memo everywhere — Adds comparison overhead. Only memoize components that re-render frequently with the same props and have expensive renders.
Forgetting to stabilize callbacks — React.memo cannot help if parent passes inline functions. Use useCallback to stabilize.
Deep comparison in custom comparator — Deep comparing large objects defeats the performance benefit. Shallow compare or compare specific fields.
Memoizing components with children — If
childrenprop is an inline JSX, it changes every render, breaking memoization.Not profiling before optimizing — Adding memo without measuring is premature optimization. Profile first, then optimize the bottlenecks.
Practice Questions
What does React.memo do? It prevents re-rendering of a component when its props have not changed (shallow comparison).
When does React.memo NOT prevent re-renders? When any prop is a new reference (new object, array, or function), or when the component uses context that changes.
How do you customize prop comparison? Pass a comparison function as the second argument:
memo(Component, (prev, next) => boolean).What must you pair React.memo with for function props? useCallback, which returns a stable function reference unless dependencies change.
When should you NOT use React.memo? For small, cheap-to-render components, or when the comparison overhead exceeds the render cost.
Challenge
Profile a ContactList component rendering 500 contacts. Identify which re-renders are unnecessary. Add React.memo + useCallback to reduce re-renders by 90% when a single contact's status changes. Verify with Profiler.
FAQ
Mini Project
Optimize a AnalyticsDashboard with React.memo. The dashboard has: DateRangePicker (frequently re-renders), ChartComponent (expensive SVG rendering), StatsCards (4 cards), DataTable (100+ rows). Apply React.memo to ChartComponent, StatsCards, and DataTable rows. Use useCallback for all event handlers. Use Profiler to verify optimization saves 50%+ render time.
What's Next
Continue with useCallback and children patterns:
React Callback, React Children, React Performance
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro