React useCallback
useCallback returns a memoized version of a function so it keeps the same reference across re-renders as long as its dependencies haven't changed.
Why Function Identity Matters
Every time a component re-renders, any function you define inside it — including inline handlers like () => doThing() — is a brand-new function object, even if its logic is identical to the previous render's. That's irrelevant for a plain <button onClick={...}>, but it matters the moment that function is a dependency of useEffect, or a prop passed to a component wrapped in React.memo, because a 'new' reference looks like a real change.
What useCallback Does
useCallback(fn, deps) returns the same function reference across renders as long as none of the values in deps have changed since the last render. Internally it's closely related to useMemo — useCallback(fn, deps) behaves like useMemo(() => fn, deps) — it just saves you writing the wrapping arrow function yourself.
- Passing a callback down to a child wrapped in React.memo, so the child doesn't re-render just because the parent re-rendered
- Using a function as a dependency of useEffect, useMemo, or another useCallback, so the effect doesn't re-run every render
- Passing a stable handler to a custom hook that relies on referential equality internally
- Debouncing or throttling a function where the wrapper must stay the same instance across renders
Without useCallback: A Memoized Child Re-renders Anyway
import React, { useState } from 'react';
const ExpensiveList = React.memo(function ExpensiveList({ onItemClick }) {
console.log('ExpensiveList rendered');
return (
<ul>
<li onClick={() => onItemClick(1)}>Item 1</li>
<li onClick={() => onItemClick(2)}>Item 2</li>
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
// A new function is created on every render...
function handleItemClick(id) {
console.log('Clicked item', id);
}
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* ...so ExpensiveList re-renders every time, even though
React.memo is supposed to skip unchanged props. */}
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
export default Parent;
With useCallback: The Child Skips Re-rendering
import React, { useState, useCallback } from 'react';
const ExpensiveList = React.memo(function ExpensiveList({ onItemClick }) {
console.log('ExpensiveList rendered');
return (
<ul>
<li onClick={() => onItemClick(1)}>Item 1</li>
<li onClick={() => onItemClick(2)}>Item 2</li>
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = useCallback((id) => {
console.log('Clicked item', id);
}, []); // no dependencies: the function never needs to change
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* handleItemClick keeps the same reference across renders,
so React.memo correctly skips re-rendering ExpensiveList. */}
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
export default Parent;
Stabilizing a Debounced Search Handler
import { useCallback, useRef, useState } from 'react';
function useDebouncedCallback(callback, delay) {
const timeoutRef = useRef(null);
return useCallback(
(...args) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => callback(...args), delay);
},
[callback, delay]
);
}
function SearchBox({ onSearch }) {
const debouncedSearch = useDebouncedCallback(onSearch, 300);
return (
<input
type="text"
placeholder="Search..."
onChange={(e) => debouncedSearch(e.target.value)}
/>
);
}
export default function App() {
const [lastSearch, setLastSearch] = useState('');
return (
<div>
<SearchBox
onSearch={(value) => {
console.log('Searching for', value);
setLastSearch(value);
}}
/>
<p>Last search: {lastSearch}</p>
</div>
);
}
Exercise: React useCallback
What does useCallback actually memoize?