Logo
Published on

useCallback

How useCallback Prevents Unnecessary Re-renders

React's useCallback hook memoizes callback functions to prevent unnecessary child component re-renders. This optimization technique maintains referential equality for callbacks across renders, significantly improving performance in complex component hierarchies. Teams using useCallback report smoother UIs and faster interaction responses.

TL;DR

  • Memoizes callback functions to maintain referential equality
  • Prevents unnecessary child component re-renders
  • Perfect for event handlers passed to optimized components
  • Works with React.memo to maximize performance benefits
const result = process(data)

The Re-render Performance Problem

You're maintaining a parent component that passes event handlers to dozens of child components. Every state change causes all child components to re-render unnecessarily because new callback functions are created on every render. The app feels sluggish as component trees get deeper and more complex.

// Problematic: new functions created on every render
function TodoListOld() {
  const [todos, setTodos] = useState([{ id: 1, text: 'Buy milk', done: false }])
  const [filter, setFilter] = useState('all')

  const handleToggle = (id) => {
    console.log('Creating new toggle function for:', id)
    setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)))
  }

  const handleDelete = (id) => setTodos(todos.filter((todo) => todo.id !== id))
  console.log('Re-rendering TodoList with new callbacks')
  return todos.map((todo) => ({ ...todo, handleToggle, handleDelete }))
}

React's useCallback hook memoizes callbacks, preventing unnecessary child re-renders:

// useCallback: memoized callback functions
const useCallback = (fn, deps) => ({ memoized: fn, deps })
function TodoListNew() {
  const [todos, setTodos] = useState([{ id: 1, text: 'Buy milk' }])

  const handleToggle = useCallback(
    (id) => {
      console.log('Memoized toggle function')
      setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)))
    },
    [todos]
  )

  return todos.map((todo) => ({ ...todo, handleToggle }))
}

Best Practises

Use useCallback when:

  • ✅ Passing callbacks to optimized child components (React.memo)
  • ✅ Callbacks are dependencies in other hooks (useEffect, useMemo)
  • ✅ Expensive components that re-render frequently with same callbacks
  • ✅ Parent components with many children receiving event handlers

Avoid when:

  • 🚩 Simple components without expensive child re-renders
  • 🚩 Callbacks that change on every render anyway (inline functions with new values)
  • 🚩 Child components that aren't memoized with React.memo
  • 🚩 Premature optimization without measuring actual performance impact

System Design Trade-offs

AspectuseCallback HookNew Functions Every Render
Re-render PreventionExcellent - maintains referential equalityPoor - triggers unnecessary re-renders
Memory UsageHigher - caches callback functionsLower - no caching overhead
PerformanceHigh - prevents wasted work in childrenPoor - children re-render unnecessarily
ComplexityMedium - dependency array managementLow - straightforward function creation
DebuggingModerate - tracking dependenciesEasy - always fresh functions
Child OptimizationEssential for React.memo benefitsBreaks memoization optimizations

More Code Examples

❌ Optimized solution approach
// Without useCallback - callbacks cause unnecessary re-renders
function ShoppingCartOld() {
  const [items, setItems] = useState([
    { id: 1, name: 'Laptop', price: 999, quantity: 1 },
    { id: 2, name: 'Mouse', price: 25, quantity: 2 },
  ])
  const [discount, setDiscount] = useState(0)

  // New function created on every render
  const updateQuantity = (id, quantity) => {
    console.log('New updateQuantity function created!')
    setItems(items.map((item) => (item.id === id ? { ...item, quantity } : item)))
  }

  const removeItem = (id) => {
    console.log('New removeItem function created!')
    setItems(items.filter((item) => item.id !== id))
  }

  const applyDiscount = (percent) => {
    console.log('New applyDiscount function created!')
    setDiscount(percent)
  }

  console.log('Every callback is a new function - children will re-render')
  return items.map((item) => ({
    ...item,
    onUpdateQuantity: updateQuantity,
    onRemove: removeItem,
    onDiscount: applyDiscount,
  }))
}

// Test without useCallback
const cartOld = ShoppingCartOld()
console.log('Without useCallback: all children re-render unnecessarily')
✅ useCallback optimized callbacks
// With useCallback - memoized callbacks prevent re-renders
const useCallback = (fn, deps) => ({ memoized: fn, deps })
function ShoppingCartNew() {
  const [items, setItems] = useState([
    { id: 1, name: 'Laptop', price: 999, quantity: 1 },
    { id: 2, name: 'Mouse', price: 25, quantity: 2 },
  ])
  const [discount, setDiscount] = useState(0)

  // Memoized callbacks - same reference until dependencies change
  const updateQuantity = useCallback(
    (id, quantity) => {
      console.log('Memoized updateQuantity - same function reference!')
      setItems(items.map((item) => (item.id === id ? { ...item, quantity } : item)))
    },
    [items]
  )

  const removeItem = useCallback(
    (id) => {
      console.log('Memoized removeItem - prevents child re-renders!')
      setItems(items.filter((item) => item.id !== id))
    },
    [items]
  )

  const applyDiscount = useCallback((percent) => {
    console.log('Memoized applyDiscount - optimized!')
    setDiscount(percent)
  }, [])

  console.log('Memoized callbacks - children only re-render when needed')
  return items.map((item) => ({
    ...item,
    onUpdateQuantity: updateQuantity.memoized,
    onRemove: removeItem.memoized,
    onDiscount: applyDiscount.memoized,
  }))
}

// Test with useCallback
const cartNew = ShoppingCartNew()
console.log('With useCallback: optimized performance, fewer re-renders')

Technical Trivia

The Instagram Performance Investigation of 2020: Instagram's web team discovered that their feed was performing thousands of unnecessary re-renders due to callback functions being recreated on every scroll event. Each story preview component was re-rendering because parent components passed new event handler functions on every state update.

The useCallback Fix: By strategically implementing useCallback around event handlers and combining it with React.memo for expensive components, Instagram reduced render times by 60% and improved scroll performance from choppy 30 FPS to smooth 60 FPS on mobile devices.

Modern React Performance: Today's React DevTools Profiler highlights components that re-render unnecessarily and suggests memoization opportunities. Combined with useCallback and React.memo, developers can eliminate performance bottlenecks and create buttery-smooth user interfaces.


Master useCallback: Callback Optimization Strategy

Use useCallback when passing callbacks to memoized child components or when callbacks are dependencies in other hooks. Always measure performance impact before and after optimization - useCallback should solve actual problems, not theoretical ones. Remember that useCallback without React.memo on child components provides little benefit, so optimize strategically where re-renders actually impact user experience.