Functions in JavaScript are objects, and when you define a function inside a component, React creates a new function on every render. This can cause performance problems when you pass functions as props to child components - they see a "new" function each time and re-render unnecessarily. The useCallback hook solves this by memoizing functions, ensuring they maintain the same reference across renders. Let's master function optimization! 🎯
The Problem: New Functions Every Render
Without useCallback, functions are recreated on every render:
// Child component wrapped in React.memo
const ExpensiveChild = React.memo(({ onClick, data }) => {
console.log('ExpensiveChild rendered')
return <button onClick={onClick}>{data}</button>
})
function Parent() {
const [count, setCount] = React.useState(0)
const [text, setText] = React.useState('')
// ❌ Problem: New function created every render!
const handleClick = () => {
console.log('Clicked!')
}
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<ExpensiveChild onClick={handleClick} data="Click me" />
</div>
)
}
// Problem:
// 1. User types in input
// 2. Parent re-renders
// 3. handleClick is recreated (new function reference)
// 4. ExpensiveChild sees "different" prop
// 5. ExpensiveChild re-renders (even though React.memo is used!)
// 6. React.memo can't prevent re-render because onClick changedWhy Functions Get Recreated:
Every time a component renders, all code inside runs again.
Function declarations/expressions create NEW function objects:const fn = () => {} ← New function every render
In JavaScript: () => {} !== () => {}
Even if they do the same thing!
The Solution: useCallback
useCallback memoizes the function, keeping the same reference:
const ExpensiveChild = React.memo(({ onClick, data }) => {
console.log('ExpensiveChild rendered')
return <button onClick={onClick}>{data}</button>
})
function Parent() {
const [count, setCount] = React.useState(0)
const [text, setText] = React.useState('')
// ✅ Solution: Memoize the function
const handleClick = React.useCallback(() => {
console.log('Clicked!')
}, []) // Empty array = same function forever
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<ExpensiveChild onClick={handleClick} data="Click me" />
</div>
)
}
// How it works:
// 1. First render: Function created, reference stored
// 2. User types in input, Parent re-renders
// 3. useCallback returns same function reference
// 4. ExpensiveChild sees "same" prop
// 5. React.memo prevents re-render (prop didn't change!)
// 6. Performance optimized!useCallback Syntax
const memoizedCallback = React.useCallback(
() => {
// Function code
doSomething(a, b)
},
[a, b] // Dependencies - recreate when these change
)
// Parts:
// 1. Callback function - The function to memoize
// 2. Dependency array - When to recreate the function
// Example with dependencies:
const handleClick = React.useCallback(() => {
console.log('Count is:', count)
}, [count]) // Recreate when count changes
// Without dependencies (stable function):
const handleClick = React.useCallback(() => {
console.log('Clicked!')
}, []) // Never recreated🤔 useCallback vs useMemo
useCallback: Returns a memoized FUNCTIONconst fn = useCallback(() => doSomething(), [deps])
useMemo: Returns a memoized VALUEconst value = useMemo(() => calculate(), [deps])
Actually, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
useCallback is just a convenience wrapper!
Common useCallback Use Cases
1. Optimizing React.memo Components
// Memoized child component
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.id)
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
)
})
function TodoList() {
const [todos, setTodos] = React.useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build project', done: false }
])
const [filter, setFilter] = React.useState('all')
// Memoize callbacks to prevent TodoItem re-renders
const handleToggle = React.useCallback((id) => {
setTodos(todos => todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
}, []) // No dependencies, uses functional update
const handleDelete = React.useCallback((id) => {
setTodos(todos => todos.filter(todo => todo.id !== id))
}, [])
return (
<div>
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="done">Done</option>
</select>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
)
}
// When filter changes, TodoItems don't re-render!
// Because onToggle and onDelete keep same reference2. Event Handlers with Dependencies
function SearchBox({ onSearch }) {
const [query, setQuery] = React.useState('')
const [category, setCategory] = React.useState('all')
// Memoize search handler with dependencies
const handleSearch = React.useCallback(() => {
onSearch({ query, category })
}, [query, category, onSearch]) // Recreate when these change
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">All</option>
<option value="products">Products</option>
<option value="users">Users</option>
</select>
<button onClick={handleSearch}>Search</button>
</div>
)
}
// handleSearch is recreated only when query, category, or onSearch change3. useEffect Dependencies
function DataFetcher({ userId }) {
const [data, setData] = React.useState(null)
// Memoize fetch function
const fetchData = React.useCallback(async () => {
const response = await fetch(`/api/users/${userId}/data`)
const data = await response.json()
setData(data)
}, [userId]) // Recreate when userId changes
React.useEffect(() => {
fetchData()
}, [fetchData]) // Safe to include fetchData in dependencies
return <div>{data?.name}</div>
}
// Without useCallback:
// - fetchData recreated every render
// - useEffect runs every render
// - Infinite loop!
//
// With useCallback:
// - fetchData stable unless userId changes
// - useEffect runs only when needed4. Custom Hooks with Callbacks
function useDebounce(callback, delay) {
const timeoutRef = React.useRef(null)
// Memoize debounced function
const debouncedCallback = React.useCallback((...args) => {
clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => {
callback(...args)
}, delay)
}, [callback, delay])
return debouncedCallback
}
// Usage:
function SearchBox() {
const [query, setQuery] = React.useState('')
const handleSearch = React.useCallback((value) => {
console.log('Searching for:', value)
// API call here
}, [])
const debouncedSearch = useDebounce(handleSearch, 500)
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value)
debouncedSearch(e.target.value)
}}
/>
)
}Interactive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
When to Use useCallback
✅ DO Use useCallback When:
- Passing to React.memo components - Prevent unnecessary re-renders
- Using in useEffect dependencies - Avoid infinite loops
- Passing to child components - When child is expensive to render
- Creating custom hooks - Return stable callback references
- Optimizing lists - Item callbacks in mapped lists
❌ DON'T Use useCallback When:
- Function is only used internally - Not passed as prop
- Child doesn't use React.memo - No benefit
- Simple event handlers - onClick for basic buttons
- Everywhere "just in case" - useCallback has overhead too
useCallback Only Works With React.memo:
useCallback alone doesn't prevent re-renders!
You need BOTH:
1. useCallback in parent (stable function reference)
2. React.memo on child (skip render if props same)
Without React.memo, child re-renders anyway, and useCallback adds overhead for nothing.
Common useCallback Mistakes
1. Missing Dependencies
function Component() {
const [count, setCount] = React.useState(0)
// ❌ Wrong - Missing count dependency
const handleClick = React.useCallback(() => {
console.log('Count is:', count) // Uses count
}, []) // Missing count!
// Result: Logs stale count value
// ✅ Correct - Include all dependencies
const handleClick = React.useCallback(() => {
console.log('Count is:', count)
}, [count]) // Include count
// ✅ Better - Use functional update (no dependency)
const increment = React.useCallback(() => {
setCount(c => c + 1) // Doesn't use count from closure
}, []) // No dependencies needed!
}2. Using Without React.memo
// ❌ Bad - useCallback without React.memo
function Parent() {
const handleClick = React.useCallback(() => {
console.log('Clicked')
}, [])
return <Child onClick={handleClick} /> // Child not memoized!
}
function Child({ onClick }) {
// Re-renders anyway because not wrapped in React.memo
return <button onClick={onClick}>Click</button>
}
// ✅ Good - Use React.memo with useCallback
const Child = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>Click</button>
})
// Now useCallback actually helps!3. Overusing useCallback
// ❌ Bad - Overusing for simple handlers
function Component() {
const [count, setCount] = React.useState(0)
const increment = React.useCallback(() => {
setCount(c => c + 1)
}, []) // Unnecessary!
const decrement = React.useCallback(() => {
setCount(c => c - 1)
}, []) // Unnecessary!
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
// ✅ Good - No memoization needed for simple handlers
function Component() {
const [count, setCount] = React.useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
)
}
// No child components, no React.memo, no need for useCallback!4. Circular Dependencies
// ❌ Problematic - Circular dependency
function Component() {
const [count, setCount] = React.useState(0)
const increment = React.useCallback(() => {
setCount(count + 1) // Uses count
}, [count]) // Depends on count
React.useEffect(() => {
increment() // Calls increment
}, [increment]) // Depends on increment
}
// Problem: count changes → increment recreated → effect runs → count changes...
// ✅ Solution 1: Use functional update
const increment = React.useCallback(() => {
setCount(c => c + 1) // No count dependency
}, [])
// ✅ Solution 2: Remove useCallback if not needed
React.useEffect(() => {
setCount(c => c + 1)
}, []) // Just do it directlyuseCallback Patterns
Pattern 1: Optimized List Items
const ListItem = React.memo(({ item, onDelete, onEdit }) => {
return (
<div>
<span>{item.name}</span>
<button onClick={() => onEdit(item.id)}>Edit</button>
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
)
})
function ItemList({ items }) {
const handleDelete = React.useCallback((id) => {
// Delete logic using functional update
setItems(items => items.filter(item => item.id !== id))
}, [])
const handleEdit = React.useCallback((id) => {
// Edit logic
console.log('Editing item:', id)
}, [])
return (
<div>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
)
}
// Only items that actually change re-render!Pattern 2: Event Handler Factory
function Form() {
const [values, setValues] = React.useState({ name: '', email: '' })
// Factory pattern: Create handler for any field
const handleChange = React.useCallback((field) => {
return (e) => {
setValues(v => ({ ...v, [field]: e.target.value }))
}
}, [])
return (
<form>
<input value={values.name} onChange={handleChange('name')} />
<input value={values.email} onChange={handleChange('email')} />
</form>
)
}
// Single memoized handler, multiple fields!Pattern 3: Combining with useRef
function Component({ onSave }) {
const [data, setData] = React.useState({})
const onSaveRef = React.useRef(onSave)
// Keep ref up to date
React.useEffect(() => {
onSaveRef.current = onSave
}, [onSave])
// Stable callback that always calls latest onSave
const handleSave = React.useCallback(() => {
onSaveRef.current(data)
}, [data]) // Only depends on data, not onSave
return <button onClick={handleSave}>Save</button>
}
// Useful when onSave prop changes frequentlyuseCallback vs useMemo vs useRef
function Component() {
const [count, setCount] = React.useState(0)
// useCallback - Memoize FUNCTION
const handleClick = React.useCallback(() => {
console.log(count)
}, [count])
// useMemo - Memoize VALUE
const doubled = React.useMemo(() => {
return count * 2
}, [count])
// useRef - Mutable value (doesn't trigger re-render)
const renderCount = React.useRef(0)
renderCount.current += 1
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Renders: {renderCount.current}</p>
<button onClick={handleClick}>Click</button>
</div>
)
}
// Summary:
// useCallback: () => value (memoized function)
// useMemo: value (memoized value)
// useRef: { current } (mutable container)Key Takeaways
useCallbackmemoizes functions- Prevents new function references on every render
- Works best with React.memo components
- Returns a memoized FUNCTION (not value)
- Use for callbacks passed to optimized child components
- Include all dependencies in the array
- Use functional updates to avoid dependencies
- Don't use without React.memo (no benefit, just overhead)
- Essential for useEffect dependencies with functions
useCallback(fn, deps)=useMemo(() => fn, deps)- Measure performance before optimizing
- Avoid overuse - adds complexity and overhead
- Completes the optimization toolkit with useMemo
What's Next?
Congratulations! You've completed the Advanced Hooks section. You now understand:
- useRef for DOM access and mutable values
- useContext for sharing state without prop drilling
- useMemo for memoizing expensive calculations
- useCallback for memoizing functions
You have a complete toolkit for building optimized, professional React applications! You know how to access DOM elements, share global state, and optimize performance with memoization. But there's still more to explore:
- Custom Hooks - Creating reusable logic
- React Patterns - Composition, render props, HOCs
- Testing - Unit and integration testing
- Advanced Patterns - Portals, error boundaries, Suspense
- And much more!
Keep building - you're well on your way to React mastery! 🎉