React - useCallback: Function Memoization

When you pass functions as props to memo() components, useCallback will memorize the function and prevent unnecessary re-renders.


The Problem of Recreated Functions

New Functions on Every Render

function ProblematicParent() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([])
  const [name, setName] = useState('')

  // ❌ PROBLEM - New function on every render!
  const addItem = (text) => {
    setItems(prev => [...prev, { id: Date.now(), text }])
  }

  // ❌ PROBLEM - New function on every render!
  const handleClick = (id) => {
    console.log('Item clicked:', id)
  }

  // ❌ PROBLEM - New function on every render!
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Form submitted')
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      {/* These components re-render even if their logical props haven't changed! */}
      <ExpensiveForm onSubmit={handleSubmit} />
      <ExpensiveList items={items} onItemClick={handleClick} />
      <ExpensiveAddButton onAdd={addItem} />
    </div>
  )
}

// These optimized components are useless because the functions always change!
const ExpensiveForm = memo(function ExpensiveForm({ onSubmit }) {
  console.log('📝 ExpensiveForm re-rendered') // ← Always called!
  return <form onSubmit={onSubmit}><button>Submit</button></form>
})

const ExpensiveList = memo(function ExpensiveList({ items, onItemClick }) {
  console.log('📝 ExpensiveList re-rendered') // ← Always called!
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.text}
        </li>
      ))}
    </ul>
  )
})

const ExpensiveAddButton = memo(function ExpensiveAddButton({ onAdd }) {
  console.log('🔘 ExpensiveAddButton re-rendered') // ← Always called!
  return <button onClick={() => onAdd('New item')}>Add Item</button>
})

useCallback

Syntax and Basic Usage

import { useCallback } from 'react'

function OptimizedParent() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([])
  const [name, setName] = useState('')

  // ✅ SOLUTION - Stable memorized function
  const addItem = useCallback((text) => {
    setItems(prev => [...prev, { id: Date.now(), text, completed: false }])
  }, []) // ← No dependencies = stable function

  // ✅ SOLUTION - Stable memorized function
  const handleClick = useCallback((id) => {
    console.log('Item clicked:', id)
    // Logic that has no external dependencies
  }, [])

  // ✅ SOLUTION - Function with dependencies
  const addItemWithPrefix = useCallback((text) => {
    const prefix = name ? `[${name}] ` : ''
    setItems(prev => [...prev, { 
      id: Date.now(), 
      text: prefix + text,
      completed: false 
    }])
  }, [name]) // ← Only recreates if name changes

  // ✅ SOLUTION - Optimized event handler
  const handleCountChange = useCallback((increment) => {
    setCount(prev => prev + increment)
  }, [])

  // ✅ SOLUTION - Deletion function
  const removeItem = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id))
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1 (non-optimized)</button>
      
      <input 
        value={name} 
        onChange={e => setName(e.target.value)} 
        placeholder="Prefix..."
      />
      
      {/* Now these components only re-render when necessary! */}
      <OptimizedButton onClick={handleCountChange} increment={1}>+1</OptimizedButton>
      <OptimizedButton onClick={handleCountChange} increment={5}>+5</OptimizedButton>
      
      <OptimizedList 
        items={items} 
        onItemClick={handleClick}
        onItemRemove={removeItem}
      />
      
      <OptimizedAddForm 
        onAdd={addItem}
        onAddWithPrefix={addItemWithPrefix}
      />
    </div>
  )
}

// Now these components only re-render if their props really change!
const OptimizedButton = memo(function OptimizedButton({ onClick, increment, children }) {
  console.log(`🔘 OptimizedButton (+${increment}) re-rendered`)
  
  return (
    <button onClick={() => onClick(increment)} style={{ margin: '5px' }}>
      {children}
    </button>
  )
})

const OptimizedList = memo(function OptimizedList({ items, onItemClick, onItemRemove }) {
  console.log('📝 OptimizedList re-rendered')
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} style={{ display: 'flex', justifyContent: 'space-between' }}>
          <span onClick={() => onItemClick(item.id)} style={{ cursor: 'pointer' }}>
            {item.text}
          </span>
          <button onClick={() => onItemRemove(item.id)}>❌</button>
        </li>
      ))}
    </ul>
  )
})

const OptimizedAddForm = memo(function OptimizedAddForm({ onAdd, onAddWithPrefix }) {
  console.log('📝 OptimizedAddForm re-rendered')
  const [input, setInput] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (input.trim()) {
      onAdd(input.trim())
      setInput('')
    }
  }

  const handleSubmitWithPrefix = (e) => {
    e.preventDefault()
    if (input.trim()) {
      onAddWithPrefix(input.trim())
      setInput('')
    }
  }

  return (
    <form>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="New item..."
      />
      <button type="submit" onClick={handleSubmit}>Add</button>
      <button type="button" onClick={handleSubmitWithPrefix}>With prefix</button>
    </form>
  )
})

Resources for Further Learning