The Problem: Repeated Work on Every Render
A component re-runs its entire function body on every render. Any expensive calculation inside it re-runs too — even if none of the values it depends on actually changed.
useMemo: Memoizing a Value
useMemo(calculateValue, dependencies) re-runs an expensive calculation only when one of its dependencies actually changes, reusing the cached result otherwise.
Memoizing an Expensive Calculation
import { useMemo } from "react";
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
console.log("Filtering...");
return products.filter((p) => p.category === filter);
}, [products, filter]);
return (
<ul>
{filteredProducts.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
}useCallback: Memoizing a Function
Every render creates brand new function instances for anything defined inside a component — including event handlers. useCallback(fn, dependencies) returns the same function reference between renders as long as its dependencies haven’t changed.
Memoizing a Callback
import { useCallback } from "react";
function TodoList({ todos }) {
const handleToggle = useCallback((id) => {
console.log("Toggled:", id);
}, []); // same function reference across every render
return todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
));
}Why Function Identity Matters
useCallback is most useful when a function is passed as a prop to a child wrapped in React.memo — without it, a new function reference on every render defeats React.memo’s comparison, causing the child to re-render anyway even though nothing meaningful changed.
useCallback Paired with React.memo
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
console.log("Rendering:", todo.text);
return <li onClick={() => onToggle(todo.id)}>{todo.text}</li>;
});
// Without useCallback on onToggle, TodoItem re-renders every time
// the parent renders, even if 'todo' itself hasn't changed.Memoization is a Performance Tool, Not a Default
useMemo and useCallback have their own small cost (comparing dependencies on every render) and add code complexity. Reach for them when you’ve identified an actual performance problem — usually via the React DevTools Profiler — not preemptively on every value and function.
React Compiler Reduces the Need for Manual Memoization
Newer versions of React ship with a compiler that can automatically add memoization where it’s beneficial, reducing how often useMemo and useCallback need to be written by hand.
Best Practice
Don’t reach for useMemo/useCallback by default. Profile first, then memoize specifically where a measured re-render or slow calculation is causing a real, noticeable problem.