useCallback: Memoizing Functions
Every time a React component renders, all the functions defined inside it are recreated — new references are allocated in memory. For most props this is invisible. But when a function is passed to a memoized child component, or used as a dependency in useEffect, a new function reference on every render causes those children to re-render or effects to re-run unnecessarily. useCallback solves this by returning a stable, memoized function reference.
The Signature
const memoizedFn = useCallback(
function fn(args) {
// function body
},
[deps] // dependency array — same as useEffect
)On the first render,
useCallbackstores the function and returns itOn subsequent renders, if the dependencies have not changed, React returns the same function reference
When a dependency changes, React creates and stores a new function
The Problem: Functions Break React.memo
React.memo wraps a component and makes it skip re-rendering if its props are identical (shallow comparison). This works perfectly for primitive props. But for function props, every render creates a new function — a new reference — so React.memo always sees a "changed" prop and re-renders.
import { useState, memo } from 'react'
// Expensive child — wrapped with React.memo
const ExpensiveButton = memo(function ExpensiveButton({ onClick, label }) {
console.log('ExpensiveButton rendered:', label)
return <button onClick={onClick}>{label}</button>
})
// ❌ Without useCallback — ExpensiveButton re-renders on every parent render
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// New function reference on every render
const handleClick = () => {
setCount(c => c + 1)
}
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
{/* Every keystroke in the input re-renders Parent, which creates a new
handleClick → React.memo sees a new prop → ExpensiveButton re-renders */}
<ExpensiveButton onClick={handleClick} label="Increment" />
</div>
)
}import { useState, useCallback, memo } from 'react'
const ExpensiveButton = memo(function ExpensiveButton({ onClick, label }) {
console.log('ExpensiveButton rendered:', label)
return <button onClick={onClick}>{label}</button>
})
// ✅ With useCallback — ExpensiveButton only re-renders when handleClick changes
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// Stable reference — same function as long as deps don't change
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, []) // no deps: setCount is stable, and we use the functional updater form
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
{/* Now typing in the input does NOT re-render ExpensiveButton */}
<ExpensiveButton onClick={handleClick} label="Increment" />
</div>
)
}Use Case 2: Function as a useEffect Dependency
When a function is listed in a useEffect dependency array, every re-render that creates a new function reference will re-run the effect. useCallback stabilizes the reference.
import { useState, useCallback, useEffect } from 'react'
// ❌ Without useCallback — effect re-runs on every render
function DataFetcher({ endpoint }) {
const [data, setData] = useState(null)
// New function every render
const fetchData = async () => {
const res = await fetch(endpoint)
setData(await res.json())
}
// Because fetchData is a new reference every render, this effect
// re-runs continuously — infinite loop!
useEffect(() => {
fetchData()
}, [fetchData]) // ← new reference every render
return <div>{JSON.stringify(data)}</div>
}
// ✅ With useCallback — effect only re-runs when endpoint changes
function DataFetcher({ endpoint }) {
const [data, setData] = useState(null)
const fetchData = useCallback(async () => {
const res = await fetch(endpoint)
setData(await res.json())
}, [endpoint]) // stable when endpoint is stable
useEffect(() => {
fetchData()
}, [fetchData]) // only re-runs when fetchData (= endpoint) changes
return <div>{JSON.stringify(data)}</div>
}Use Case 3: Ref Callback for DOM Measurement
A ref callback is a function passed to the ref prop. React calls it with the DOM node when it mounts, and with null when it unmounts. Without useCallback, a new function reference on every render causes the ref callback to fire on every render — which is usually not what you want.
import { useState, useCallback } from 'react'
function MeasuredBox() {
const [height, setHeight] = useState(0)
// useCallback ensures this is the same function — React only calls
// the ref callback when the node mounts/unmounts, not on every render.
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, []) // no deps — only needs to run when the element mounts
return (
<div>
<div ref={measuredRef} style={{ padding: 20 }}>
<p>Resize the window to see changes</p>
</div>
<p>Box height: {height}px</p>
</div>
)
}Before and After: A Complete Example
A TodoList with a memoized row component. Without useCallback, every interaction causes all rows to re-render.
import { useState, useCallback, memo } from 'react'
// Memoized row — only re-renders when its own props change
const TodoRow = memo(function TodoRow({ todo, onToggle, onDelete }) {
console.log('Rendering row:', todo.id)
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
)
})
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Write tests', done: false },
{ id: 3, text: 'Deploy app', done: false },
])
const [newText, setNewText] = useState('')
// Stable references — handlers only change if setTodos changes (it never does)
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
}, [])
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id))
}, [])
function handleAdd() {
if (!newText.trim()) return
setTodos(prev => [...prev, { id: Date.now(), text: newText, done: false }])
setNewText('')
}
return (
<div>
<input value={newText} onChange={e => setNewText(e.target.value)} />
<button onClick={handleAdd}>Add</button>
<ul>
{todos.map(todo => (
<TodoRow
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
)
}Typing in the "add" input changes newText, which re-renders TodoList. Without useCallback, handleToggle and handleDelete get new references → all TodoRow components re-render. With useCallback, the handlers are stable → only the specific row that changed would need to re-render.
When NOT to Use useCallback
Scenario | Use useCallback? |
|---|---|
Function passed to React.memo child | Yes — necessary for memo to work |
Function in useEffect deps array | Yes — prevents infinite re-runs |
Ref callback (DOM measurement) | Yes — prevents extra ref calls |
Inline handler not passed to memo child | No — overhead not worth it |
Function only used in the same component | No — not passed anywhere |