React’s useMemo and useCallback hooks are powerful tools for optimizing performance, but they often lead to confusion. When should you use one over the other? Are they always necessary? And what exactly are they “memoizing”?
The Core Problem: unnecessary Renders
At the heart of useMemo and useCallback lies a common React performance bottleneck: unnecessary re-renders.
Every time a component’s state or props change, React re-renders that component and, by default, all of its children. This is usually fast, but if a child component is “expensive” (e.g., performs complex calculations, renders large lists, or frequently re-renders its own tree), these unnecessary updates can impact to performance.
Let say that you have a component that performs rendering on a complex list. If this component re-renders every time when its parent re-renders, even if the props relevant to that calculation or this list haven’t changed, you’re doing a lot of redundant work. This is where memoization comes in.
Memoization
Memoization is an optimization technique used to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
In React, useMemo and useCallback do exactly this. They takes two arguments: “create” function (returns function or value that you want to memoize) and dependency array (a list of values that the “create” function depends on).
useMemo is a Hook that lets you cache the result of a calculation between re-renders (Memoizing Values)
import { useState, useMemo } from 'react'
function ProductList({ products, filterText }) {
const filteredProducts = useMemo(() => {
console.log('Filtering products...')
return products.filter(product =>
product.name.toLowerCase().includes(filterText.toLowerCase())
);
}, [products, filterText])
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
function App() {
const [searchText, setSearchText] = useState('')
const allProducts = [
{ id: 1, name: 'Laptop' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Keyboard' },
{ id: 4, name: 'Monitor' },
]
return (
<div>
<input
type='text'
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder='Search products'
/>
<ProductList products={allProducts} filterText={searchText} />
</div>
)
}
export default App
useCallback is a Hook that lets you cache a function definition between re-renders (Memoizing Functions)
import React, { useState, useCallback } from 'react'
function Counter() {
const [count, setCount] = useState(0)
// Without useCallback, handleIncrement and handleDecrement would be a new function on every render
const handleIncrement = useCallback(() => {
setCount(prevCount => prevCount + 1)
}, []) // Empty dependency array means this function never changes
const handleDecrement = useCallback(() => {
setCount(prevCount => prevCount - 1)
}, [])
console.log('Component rendered')
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
}
export default Counter
When to Choose Which
useMemofor values,useCallbackfor functions.- Choose
useMemowhen you want to avoid re-calculating an expensive value. ChooseuseCallbackwhen you want to avoid re-creating a function instance, primarily to prevent unnecessary re-renders of memoized child components or to optimizeuseEffectdependencies.
Important Considerations and Potential Pitfalls
- Don’t Overuse Them: Memoization comes with its own overhead. React needs to store the cached values/functions and compare dependencies. If the calculation or function recreation is very cheap, the overhead of
useMemo/useCallbackmight outweigh the benefits. Profile your application to identify actual performance bottlenecks before aggressively applying these hooks. - Correct Dependency Arrays: This is crucial. If you omit a dependency or provide an incorrect one, your memoized value or function might become “stale,” leading to bugs. Always ensure your dependency array accurately reflects all values from the component’s scope that are used inside the memoized function or value calculation. ESLint’s
exhaustive-depsrule is your friend here! - Referential Equality: Remember that
useMemoanduseCallbackwork based on referential equality. If you’re comparing objects or arrays in your dependencies, a new object/array literal will always be considered different, even if its contents are identical.
