Skip to content

When to Use useCallback and useMemo in React

I used to wrap everything in useCallback and useMemo. Every function, every calculation, every variable. I thought I was being a good React developer, optimizing my components for maximum performance.

Then I realized I was actually hurting performance.

Memoization isn’t free. Every useCallback and useMemo call adds overhead - React has to compare dependencies on every render. Used incorrectly, these hooks make your app slower, not faster.

Let me show you when to actually use them.

The Direct Answer

Here’s the decision framework I wish someone had given me:

Use useMemo when:

  • You have expensive calculations (filtering large arrays, complex transformations)
  • You need to maintain referential equality for dependencies
  • The calculation runs frequently with the same inputs

Use useCallback when:

  • You’re passing functions to memoized child components (React.memo)
  • The function is a dependency in another hook’s dependency array
  • You need to prevent unnecessary re-renders in child components

That’s it. Most of the time, you don’t need either.

Why Memoization Has Costs

Before diving into examples, understand what happens when you use these hooks:

ExpensiveComponent.jsx
function ExpensiveComponent({ items }) {
// useMemo stores the result and compares dependencies
const sorted = useMemo(() => {
return items.sort((a, b) => a.value - b.value)
}, [items])
return <List data={sorted} />
}

On every render:

  1. React compares items with the previous items (reference equality check)
  2. If different, it runs the sort function
  3. If same, it returns the cached result

This comparison happens on every single render. If items changes frequently, you’re paying for comparison overhead without getting any benefit.

When useMemo Actually Helps

Here’s a real example where useMemo makes sense:

ProductList.jsx
function ProductList({ products, filterText }) {
// This could be expensive with thousands of products
const filteredProducts = useMemo(() => {
console.log('Filtering products...') // See how often this runs
return products.filter(product =>
product.name.toLowerCase().includes(filterText.toLowerCase())
)
}, [products, filterText])
return (
<div>
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}

This is worth memoizing because:

  • The filter runs on every render otherwise
  • With 10,000 products, this could take noticeable time
  • The dependencies (products, filterText) don’t change on every render

But if you only have 10 products? Don’t bother. The overhead isn’t worth it.

When useCallback Actually Helps

The most common valid use case is passing functions to memoized children:

ParentComponent.jsx
const ChildComponent = React.memo(function ChildComponent({ onClick, data }) {
console.log('Child rendered')
return <button onClick={onClick}>{data}</button>
})
function ParentComponent() {
const [count, setCount] = useState(0)
const [data, setData] = useState('Click me')
// Without useCallback, this function is recreated every render
// Which breaks React.memo's optimization
const handleClick = useCallback(() => {
console.log('Clicked!')
}, []) // No dependencies = never changes
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ChildComponent onClick={handleClick} data={data} />
</div>
)
}

Without useCallback, clicking the “Count” button recreates handleClick, causing ChildComponent to re-render even though React.memo should prevent it.

Common Mistake #1: Premature Optimization

I see this constantly:

UnnecessaryMemo.jsx
function SimpleComponent({ name }) {
// This is pointless - string concatenation is already fast
const greeting = useMemo(() => `Hello, ${name}`, [name])
return <div>{greeting}</div>
}

String concatenation takes microseconds. The useMemo overhead might actually be more expensive than the operation itself.

Rule: Only memoize expensive operations. When in doubt, measure with console.time() or React DevTools Profiler.

Common Mistake #2: Wrapping Everything

This is what I used to do:

OverOptimized.jsx
function TodoList({ todos }) {
// All of these are unnecessary
const todoCount = useMemo(() => todos.length, [todos])
const isEmpty = useMemo(() => todos.length === 0, [todos])
const headerText = useMemo(() => isEmpty ? 'No todos' : 'Your Todos', [isEmpty])
return (
<div>
<h1>{headerText}</h1>
<p>Count: {todoCount}</p>
</div>
)
}

These are all trivial calculations. You’re adding complexity without benefit.

Rule: Start without memoization. Add it only when you identify a performance problem.

Common Mistake #3: Wrong Dependencies

This creates bugs:

WrongDependencies.jsx
function SearchResults({ query, results }) {
// Bug: 'query' is missing from dependencies
const filtered = useMemo(() => {
return results.filter(item =>
item.name.includes(query)
)
}, [results]) // Should be [results, query]
return <ResultList items={filtered} />
}

If query changes but results doesn’t, filtered won’t update. You’ll show stale results.

Rule: Always include all values from the component scope that the memoized function uses. ESLint’s exhaustive-deps rule helps catch this.

Common Mistake #4: Memoizing Without React.memo

This does nothing:

IneffectiveMemo.jsx
function ParentComponent() {
const [count, setCount] = useState(0)
// This useCallback is useless if ChildComponent isn't memoized
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ChildComponent onClick={handleClick} />
</div>
)
}
// Not memoized - re-renders whenever parent re-renders
function ChildComponent({ onClick }) {
return <button onClick={onClick}>Click me</button>
}

ChildComponent re-renders whenever ParentComponent does, regardless of whether handleClick changed.

Rule: useCallback only helps when the child component is wrapped in React.memo.

The Connection to React.memo

React.memo, useCallback, and useMemo work together:

MemoChain.jsx
// Step 1: Memoize the component
const ExpensiveItem = React.memo(function ExpensiveItem({ item, onUpdate }) {
console.log('ExpensiveItem rendered')
return (
<div>
{item.name}
<button onClick={() => onUpdate(item.id)}>Update</button>
</div>
)
})
function ItemList({ items }) {
// Step 2: Memoize the callback to prevent it from changing
const handleUpdate = useCallback((id) => {
console.log('Updating item:', id)
}, [])
// Step 3: Memoize expensive filtering
const visibleItems = useMemo(() => {
return items.filter(item => item.visible)
}, [items])
return (
<div>
{visibleItems.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onUpdate={handleUpdate}
/>
))}
</div>
)
}

All three pieces work together:

  • useMemo prevents expensive filtering on every render
  • useCallback keeps handleUpdate stable
  • React.memo prevents re-rendering ExpensiveItem when props haven’t changed

A Practical Decision Framework

Next time you’re wondering whether to memoize, ask yourself:

For useMemo:

  1. Is this calculation expensive? (Test it!)
  2. Does it run frequently?
  3. Do the dependencies change less often than the component renders?

If yes to all three, use useMemo.

For useCallback:

  1. Is this function passed to a React.memo component?
  2. Is this function a dependency in another hook?
  3. Does preventing re-renders actually matter for performance?

If yes, use useCallback.

Otherwise, skip the memoization. Your code will be simpler and often faster.

What I Do Now

These days, I follow a simple rule: don’t memoize until you measure.

  1. Write the simple version first
  2. Use React DevTools Profiler to identify actual performance issues
  3. Add memoization only where it makes a measurable difference

Most of the time, React is fast enough without optimization. When it’s not, useMemo and useCallback are tools for specific problems - not habits to apply everywhere.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments