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>
)
})