Prevent Unnecessary Renders in React: A Guide to React.memo, useMemo, and useCallback


In React applications, rendering performance can quickly become a challenge—especially as your component tree grows. Without proper optimizations, even minor state changes can trigger unnecessary re-renders, leading to sluggish UIs and wasted processing.
In this article, we'll explore three powerful tools React gives us to tackle this challenge:
React.memo — memoize components
useMemo — memoize expensive computations
useCallback — memoize functions
We'll break down how each of these features works, explore when and why to use them, and demonstrate real-life use cases with clean, practical examples. Whether you're building a todo list, a shopping cart, or a complex dashboard, these patterns can help you write faster and more efficient React code.
Let’s dive into how to prevent unnecessary re-renders in React, and take your performance to the next level.
1. Optimizing with React.memo
What is React.memo?
React.memo
or memo
is a higher-order component that memoizes a functional component, preventing it from re-rendering unless its props change.
React normally re-renders a component whenever its parent re-renders. With memo
, you can create a component that React will not re-render when its parent re-renders so long as its new props are the same as the old props. Such a component is said to be memoized.
To memoize a component, wrap it in memo
and use the value that it returns in place of your original component.
import React, { useState } from "react";
const ChildComponent = React.memo(({ value }) => {
console.log("Child component rendered");
return <p>Child Value: {value}</p>;
});
function MemoExample() {
const [count, setCount] = useState(0);
const [childValue, setChildValue] = useState(100);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<ChildComponent value={childValue} />
<button onClick={() => setChildValue(childValue + 10)}>
Update Child Value
</button>
</div>
);
}
export default MemoExample;
How It Works:
Clicking Increment Count re-renders the parent.
The ChildComponent does not re-render unless
childValue
changes.Saved re-renders = Better performance!
A React component should always have pure rendering logic. This means that it must return the same output if its props, state, and context haven’t changed. By using memo
, you are telling React that your component complies with this requirement, so React doesn’t need to re-render as long as its props haven’t changed.
More Examples :
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
});
In this example, notice that the Greeting
component re-renders whenever name
is changed (because that’s one of its props), but not when address
is changed (because it’s not passed to Greeting
as a prop).
Warning :
“Even when a component is memoized, it will still re-render when its own state and context value changes. Memoization only has to do with props that are passed to the component from its parent.“
2. Optimizing with useMemo
What is useMemo?
useMemo
memoizes the result of a function or cache the result of a calculation between re-renders, recomputing the value only when its dependencies change.
Perfect for expensive calculations!
You need to pass two things to useMemo
:
A calculation function that takes no arguments, like
() =>
, and returns what you wanted to calculate.A list of dependencies including every value within your component that’s used inside your calculation.
On every subsequent render, React will compare the dependencies with the dependencies you passed during the last render. If none of the dependencies have changed, useMemo
will return the value you already calculated before. Otherwise, React will re-run your calculation and return the new value.
In other words, useMemo
caches a calculation result between re-renders until its dependencies change.
By default, React will re-run the entire body of the component every time that it re-renders. For example, if any child component updates its state or receives new props from its parent, the entire function(expensive calculation) inside this component will re-run.
Usually, this isn’t a problem because most calculations are very fast. However, if you’re filtering or transforming a large array, or doing some expensive computation, you might want to skip doing it again if data hasn’t changed.
Example :
import React, { useState, useMemo } from "react";
const expensiveCalculation = (num) => {
console.log("Running expensive calculation...");
for (let i = 0; i < 1000; i++) {} // Simulate heavy work
return num * 2;
};
function UseMemoExample() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(5);
const computedValue = useMemo(() => expensiveCalculation(number), [number]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<h2>Computed Value: {computedValue}</h2>
<button onClick={() => setNumber(number + 1)}>Change Number</button>
</div>
);
}
export default UseMemoExample;
The expensiveCalculation only runs when
number
changes.Updating
count
does not trigger a costly recalculation.
Combo : useMemo and memo
React’s smartest optimization trick isn’t complex — it’s simply knowing when to combine useMemo
and memo
.
Using useMemo
and memo
together in React creates a powerful optimization strategy. While useMemo
caches expensive calculations like filtering or sorting, memo
prevents components from re-rendering unless their props actually change. By combining both, you can dramatically reduce unnecessary work — minimizing recalculations and avoiding wasteful DOM updates. This leads to smoother, faster applications, especially when working with large lists, complex UIs, or frequent state changes.
A simple project can show you the real power.
App.jsx
import { useState, useMemo } from 'react';
import TodoList from './TodoList';
const initialTodos = [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Learn useMemo and memo', completed: false },
{ id: 3, text: 'Build a Todo App', completed: false },
];
export default function App() {
const [todos, setTodos] = useState(initialTodos);
const [tab, setTab] = useState('all');
const [theme, setTheme] = useState('light');
function handleAddTodo() {
const text = prompt('New todo:');
if (text) {
setTodos([
...todos,
{ id: todos.length + 1, text, completed: false }
]);
}
}
function handleToggleTheme() {
setTheme(theme === 'light' ? 'dark' : 'light');
}
return (
<div className={`app ${theme}`}>
<h1>Todo App</h1>
<div className="buttons">
<button onClick={() => setTab('all')}>All</button>
<button onClick={() => setTab('active')}>Active</button>
<button onClick={() => setTab('completed')}>Completed</button>
<button onClick={handleAddTodo}>Add Todo</button>
<button onClick={handleToggleTheme}>
Toggle {theme === 'light' ? 'Dark' : 'Light'} Theme
</button>
</div>
<TodoList todos={todos} tab={tab} theme={theme} />
</div>
);
}
Explanation:
App.jsx
manages the core application state: the todos, the current filter tab, and the theme. It provides handlers to add a todo or toggle between light and dark mode. All states are passed to TodoList
for further management.
TodoList.jsx
import { useMemo } from 'react';
import List from './List';
function filterTodos(todos, tab) {
if (tab === 'active') {
return todos.filter(todo => !todo.completed);
} else if (tab === 'completed') {
return todos.filter(todo => todo.completed);
}
return todos;
}
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
return (
<div className={`todo-list ${theme}`}>
<List items={visibleTodos} />
</div>
);
}
Explanation:
TodoList.jsx
filters todos according to the selected tab using filterTodos
, then uses useMemo
to memorize the filtered result, preventing unnecessary recalculations.
List.jsx
import { memo } from 'react';
import TodoItem from './TodoItem';
const List = memo(function List({ items }) {
console.log('Rendering <List />');
if (items.length === 0) {
return <p>No todos</p>;
}
return (
<ul>
{items.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
});
export default List;
Explanation:
List.jsx
renders a list of todos, and is wrapped with memo
to avoid re-rendering unless the items
array changes.
TodoItem.jsx
import { memo } from 'react';
const TodoItem = memo(function TodoItem({ todo }) {
console.log('Rendering <TodoItem />:', todo.text);
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</li>
);
});
export default TodoItem;
Explanation:
TodoItem.jsx
renders a single todo with a strikethrough if completed. Memoization prevents it from re-rendering unless the individual todo
actually changes.
3. Optimizing with useCallback
What is useCallback?
useCallback
memoizes a function, keeping its reference stable between renders unless dependencies change.
Useful when passing callbacks to memoized components (React.memo
) — otherwise, new function references would trigger unnecessary re-renders.
Example:
import React, { useState, useCallback } from "react";
const Button = React.memo(({ handleClick }) => {
console.log("Button component rendered");
return <button onClick={handleClick}>Click Me</button>;
});
function UseCallbackExample() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<h1>Count: {count}</h1>
<Button handleClick={handleClick} />
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something..."
/>
</div>
);
}
export default UseCallbackExample;
What Happens Without useCallback :
When text state changes (e.g., typing in the input field), UseCallbackExample component re-renders.
The
handleClick
function is recreated, generating a new function reference.Since
Button
receives a newhandleClick
prop,React.memo
detects a prop change and re-renders the Button component, even though nothing visible has changed.
What Happens With useCallback :
When text state changes, UseCallbackExample still re-renders.
However, useCallback memoizes the function, maintaining the same function reference unless the dependencies change.
The
Button
component receives the samehandleClick
function reference, and React.memo prevents unnecessary re-rendering.
A real-life example
When combined, useCallback
and memo
form a powerful optimization strategy in React. useCallback
keeps function references stable across renders, preventing child components from re-rendering unnecessarily when functions are passed as props. Meanwhile, memo
ensures components only re-render when their props actually change. This duo is especially useful for passing event handlers—like onClick
or onToggle
—to deeply nested components. Together, they help reduce wasted renders, improve performance, and keep UIs smooth and scalable. Let’s see it in a real-life simple example.
App.jsx
import { useState, useCallback } from 'react';
import ProductList from './ProductList';
import Cart from './Cart';
const initialProducts = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 499 },
{ id: 3, name: 'Headphones', price: 199 },
];
export default function App() {
const [cartItems, setCartItems] = useState([]);
const handleAddToCart = useCallback((product) => {
setCartItems((prevItems) => [...prevItems, product]);
}, []);
return (
<div className="app">
<h1>Shopping Cart</h1>
<ProductList products={initialProducts} onAddToCart={handleAddToCart} />
<Cart cartItems={cartItems} />
</div>
);
}
Explanation:
The App.jsx
file initializes a list of products and manages the cart items. It defines the handleAddToCart
function, memoized with useCallback
, so the function identity stays stable across re-renders, ensuring child components relying on this function don't unnecessarily re-render.
ProductList.jsx
import ProductItem from './ProductItem';
import { memo } from 'react';
const ProductList = memo(function ProductList({ products, onAddToCart }) {
console.log('Rendering <ProductList />');
return (
<div className="product-list">
{products.map(product => (
<ProductItem key={product.id} product={product} onAddToCart={onAddToCart} />
))}
</div>
);
});
export default ProductList;
Explanation:
ProductList.jsx
renders a list of products and is memoized using memo
, ensuring it only re-renders when products
or onAddToCart
props change.
ProductItem.jsx
import { memo } from 'react';
const ProductItem = memo(function ProductItem({ product, onAddToCart }) {
console.log('Rendering <ProductItem />:', product.name);
return (
<div className="product-item">
<h2>{product.name}</h2>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
</div>
);
});
export default ProductItem;
Explanation:
ProductItem.jsx
represents a single product and offers an "Add to Cart" button
. It’s wrapped in memo
, ensuring it re-renders only if its own props change.
Cart.jsx
import { memo } from 'react';
const Cart = memo(function Cart({ cartItems }) {
console.log('Rendering <Cart />');
if (cartItems.length === 0) {
return <p>Your cart is empty.</p>;
}
return (
<div className="cart">
<h2>Cart</h2>
<ul>
{cartItems.map((item, index) => (
<li key={index}>{item.name} - ${item.price}</li>
))}
</ul>
</div>
);
});
export default Cart;
Explanation:
Cart.jsx
renders the list of items added to the cart. It’s wrapped in memo
so it only re-renders when the cartItems
prop changes.
When Should You Use useCallback?
When passing functions as props to memoized components (
React.memo
).When using functions in dependencies of
useEffect
,useMemo
, or otherhooks
.When performance matters, e.g., in large component trees or frequent re-renders.
Why it is important ?
React.memo + useCallback = Prevents unnecessary child component re-renders when passing functions as props.
Without useCallback, the function reference changes on every render, causing
React.memo
to fail in preventing re-renders.
Final Thoughts
With the help of real-world examples and hands-on code, you’ve seen how to prevent unnecessary re-renders and make your React apps smoother and more efficient. Whether you're working on a simple feature or architecting a complex UI, these techniques (React.memo
, useMemo
, and useCallback
) will serve you well.
Keep exploring, keep experimenting—and remember: performance isn’t just about speed, it’s about writing smarter code that does only what it needs to.
Subscribe to my newsletter
Read articles from Md. Monirul Islam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Md. Monirul Islam
Md. Monirul Islam
Passionate and detail-oriented React Developer with over 5 years of industry experience in building scalable web and mobile applications using modern technologies such as React.js, Next.js, TypeScript, and Node.js. Proven track record of delivering high-quality solutions in the fintech, real estate, and government sectors.