ResourcesReact PatternsReact Performance: useMemo, useCallback, and When NOT to Use Them
⚛️React PatternsReact Performance: useMemo, useCallback, and When NOT to Use Them7 min

React Performance: useMemo, useCallback, and When NOT to Use Them

Most developers overuse useMemo and useCallback. Here's the real mental model for when memoization helps, and when it hurts.

📅January 27, 2026TechTwitter.ioreactperformanceusememousecallback

The Memoization Trap

Junior React developers don't use useMemo and useCallback. Senior React developers overuse them. The goal is to use them exactly when they help.

The key insight: React is already fast. Unnecessary renders are rarely the problem.


What These Hooks Actually Do

// useMemo: memoize a computed value
const sortedItems = useMemo(() => {
  return items.slice().sort(compareFn)
}, [items, compareFn])

// useCallback: memoize a function reference
const handleClick = useCallback(() => {
  doSomething(id)
}, [id])

Both cache their result until their dependencies change. This is useful only if:

  1. The thing being cached is expensive to compute, OR
  2. The cached thing is passed to a component that checks reference equality (i.e., it's wrapped in React.memo)

If neither condition is true, you're adding overhead with no benefit.


When NOT to Use useMemo

// ❌ Pointless memoization — filtering a small array is free
const filteredItems = useMemo(() => {
  return items.filter(i => i.active)
}, [items])

// ✅ Just do it
const filteredItems = items.filter(i => i.active)
// ❌ Pointless — object creation is cheap
const style = useMemo(() => ({
  color: 'red',
  fontSize: '16px',
}), [])

// ✅ Move it outside the component or just inline it
const STYLE = { color: 'red', fontSize: '16px' }

The memoization itself has a cost: React has to store the cached value and compare dependencies on every render. For cheap operations, this overhead exceeds the savings.


When NOT to Use useCallback

// ❌ Pointless — Parent re-renders anyway, ChildComponent isn't memo'd
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return <ChildComponent onClick={handleClick} />  // Not React.memo'd
}

If ChildComponent isn't wrapped in React.memo, it re-renders when Parent re-renders regardless. The stable function reference from useCallback does nothing.


When TO Use useMemo

Expensive computation

// ✅ Sorting/filtering large datasets is expensive
const processedData = useMemo(() => {
  return rawData
    .filter(item => item.date > cutoff)
    .sort((a, b) => b.value - a.value)
    .slice(0, 100)
}, [rawData, cutoff])

Stable reference for React.memo'd components

const MemoChild = React.memo(({ config }) => <ExpensiveChart config={config} />)

function Parent({ data }) {
  // ✅ Without useMemo, config is a new object every render
  // React.memo sees a different reference and re-renders
  const config = useMemo(() => ({
    data,
    theme: 'dark',
    animations: true,
  }), [data])

  return <MemoChild config={config} />
}

When TO Use useCallback

Stable callback for React.memo'd children

const ListItem = React.memo(({ item, onDelete }) => (
  <div>
    {item.name}
    <button onClick={onDelete}>Delete</button>
  </div>
))

function List({ items }) {
  // ✅ Without useCallback, onDelete is a new function every render
  // Each ListItem re-renders even though nothing changed for it
  const handleDelete = useCallback((id) => {
    deleteItem(id)
  }, [])

  return items.map(item =>
    <ListItem key={item.id} item={item} onDelete={() => handleDelete(item.id)} />
  )
}

Stable dependency for other hooks

// ✅ Without useCallback, fetchData changes every render
// useEffect would re-run on every render
const fetchData = useCallback(async () => {
  const data = await api.get('/data')
  setData(data)
}, [])

useEffect(() => {
  fetchData()
}, [fetchData])

The Decision Framework

Before adding useMemo or useCallback, ask:

  1. Is this computation actually expensive? Profile it. Don't guess.
  2. Is the result passed to a React.memo'd component? If not, stable reference doesn't help.
  3. Is this a useEffect dependency that needs to be stable? Then useCallback is appropriate.

If none of these are true: skip it.


React.memo: Use It Sparingly Too

// ✅ Good use of React.memo: expensive component, props rarely change
const DataGrid = React.memo(({ rows, columns, onSort }) => {
  // Renders thousands of cells
  return <ComplexGrid rows={rows} columns={columns} onSort={onSort} />
})

// ❌ Pointless React.memo: cheap component, props change constantly
const Badge = React.memo(({ count }) => <span>{count}</span>)

The Real Performance Wins

Before reaching for useMemo/useCallback:

  1. Move to Server Components — don't render it on the client at all
  2. Code split — lazy load heavy components
  3. Virtualize long listsreact-window or react-virtual
  4. Fix the real bottleneck — profile with React DevTools Profiler first

Key Takeaways

  • useMemo/useCallback add overhead — they help only in specific scenarios
  • They're useful for: expensive computations, stable refs for React.memo components, stable useEffect dependencies
  • Wrap a child in React.memo only if it's expensive AND its props are stable
  • Always profile before optimizing — React is fast and you might be solving the wrong problem
  • Server Components are the best performance optimization for data-heavy UI