useMemo vs useCallback [ Simplest way explained ]


React's rendering behavior can sometimes be a performance bottleneck in your applications. Every time a component re-renders, React recreates almost everything inside it. Let me break down how React's memoization hooks can help solve this problem, and when you should use them.
The Re-rendering Problem
When a React component re-renders, here's what happens:
Every function gets redefined
Every constant gets recreated
Every object gets a new memory address
This means they all get new locations in memory, even if their values haven't changed. The only exceptions are values managed by useState
and the effect functions in useEffect
(though these still get re-mounted and unmounted according to their lifecycle).
By "redefined" or "reallocated," I mean these items get assigned new addresses in memory. This constant recreation can impact performance, especially in complex applications.
Enter Memoization: useMemo, useCallback, and memo()
React provides three powerful tools to optimize this behavior:
useCallback: Memoizing Functions
useCallback
preserves function references between renders. Instead of creating a new function on each render, it returns the same function reference unless its dependencies change.
// Without useCallback - new function reference on every render
const handleClick = () => {
console.log(count);
};
// With useCallback - same function reference between renders
const handleClick = useCallback(() => {
console.log(count);
}, [count]); // Only changes when count changes
useMemo: Memoizing Values
useMemo
caches the result of calculations between renders. It's perfect for expensive operations that shouldn't run on every render.
// Without useMemo - recalculates on every render
const expensiveValue = calculateSomethingExpensive(data);
// With useMemo - only recalculates when dependencies change
const expensiveValue = useMemo(() => {
return calculateSomethingExpensive(data);
}, [data]); // Only recalculates when data changes
memo(): Memoizing Components
memo()
prevents unnecessary re-renders of entire components. A memoized component only re-renders if its props actually change.
// Define a component
function MyComponent(props) {
// Component logic
}
// Export a memoized version
export default memo(MyComponent);
The Tradeoff: Time vs. Space
Memoization is essentially a time-space tradeoff:
Benefit: Saves processing time by avoiding unnecessary recalculations and re-renders
Cost: Uses more memory to store the memoized values or functions
This is why memoizing everything is a bad practice. You're essentially trading RAM for CPU time, which isn't always beneficial.
When to Use Each Type of Memoization
Use useCallback when:
Passing functions to memoized child components
A function is a dependency in another hook
Functions are complex or create closures over many values
Use useMemo when:
Calculating values is computationally expensive
Derived values are used in multiple places in your component
You need to maintain referential equality for objects or arrays
Use memo() when:
Components render often but rarely need to update
Components have expensive rendering logic
You're rendering many instances of the same component (like in a list)
A Real-World Example
Imagine rendering a long list of items where only one item changes:
// Parent component
function TodoList({ todos, toggleTodo }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={useCallback(() => toggleTodo(todo.id), [todo.id, toggleTodo])}
/>
))}
</ul>
);
}
// Child component
const TodoItem = memo(function TodoItem({ todo, onToggle }) {
console.log(`Rendering todo: ${todo.text}`);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
{todo.text}
</li>
);
});
With this setup, when one todo changes, only that specific TodoItem
component re-renders, not the entire list.
Conclusion
React's memoization hooks are powerful tools for performance optimization, but they should be used judiciously. Memoize components and calculations that are genuinely expensive or cause unnecessary re-renders, but DON’T overuse these tools for simple operations.
Remember that premature optimization can lead to more complex, harder-to-maintain code. Start with clean, readable code first, then optimize with memoization where you identify actual performance bottlenecks.
Subscribe to my newsletter
Read articles from Ayan Masood directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ayan Masood
Ayan Masood
Full Stack Web & App Developer, fostering tech communities, delivering talks at tech events while managing my own startup