When
For a while, useCallback felt unnecessary.
I understood what it did—it keeps the same function reference between renders—but I couldn’t really see when it actually matters.
Most examples felt too simple.
A button click here, a small component there.
Nothing that justified the extra complexity.
The Setup That Changed It
Things became clearer when I looked at a more realistic case:
A search page.
- an input to filter data
- a large list of items
- each item is its own component
To keep performance under control, each item is wrapped with React.memo.
const ListItem = React.memo(({ item, onSelect }) => {
console.log("render item:", item.id)
return (
<div onClick={() => onSelect(item)}>
{item.name}
</div>
)
})The Initial Implementation
In the parent component:
function App() {
const [query, setQuery] = useState("")
const [selected, setSelected] = useState(null)
const items = useMemo(() => {
return heavyFilter(data, query)
}, [query])
return (
<>
<input onChange={e => setQuery(e.target.value)} />
{items.map(item => (
<ListItem
key={item.id}
item={item}
onSelect={(item) => setSelected(item)}
/>
))}
</>
)
}Everything works.
But there’s a subtle issue.
What Actually Happens
Every time you type:
- <code>App</code> re-renders
- a new <code>onSelect</code> function is created
- each <code>ListItem</code> receives a new function prop
Even though:
- the list items didn’t change
- only the input value changed
React sees:
“new function → props changed → re-render”
So all items render again.
Why This Is a Problem
If the list is small, you won’t notice.
But with:
- hundreds of items
- expensive rendering
This becomes:
unnecessary work on every keystroke
The Fix With useCallback
function App() {
const [query, setQuery] = useState("")
const [selected, setSelected] = useState(null)
const items = useMemo(() => {
return heavyFilter(data, query)
}, [query])
const handleSelect = useCallback((item) => {
setSelected(item)
}, [])
return (
<>
<input onChange={e => setQuery(e.target.value)} />
{items.map(item => (
<ListItem
key={item.id}
item={item}
onSelect={handleSelect}
/>
))}
</>
)
}What Changed
Now:
- <code>handleSelect</code> stays the same across renders
- <code>ListItem</code> props don’t change
- <code>React.memo</code> can actually do its job
So when typing:
- only the parent re-renders
- list items stay untouched
The Important Realization
The issue wasn’t about the function itself.
It was about:
the function being treated as a new value every time
Why useCallback Matters Here
Not because the function is expensive.
But because:
its identity affects other components
Without useCallback:
- identity changes → props change → re-render
With useCallback:
- identity stays → props stay → skip render
The Shift in Thinking
At first, useCallback feels like:
“avoid recreating functions”
But that’s not the real benefit.
The real benefit is:
controlling when React considers something “the same”
Final Takeaway
You don’t use useCallback for every function.
You use it when:
- the function is passed to other components
- those components care about prop identity
- unnecessary re-renders become a problem
Because in the end, it’s not about functions.
It’s about how React decides:
whether something changed—or not.