When useCallback Actually Starts to Matter
useCallback becomes useful when a function is passed to memoized child components. Without it, a new function is created on every render, causing unnecessary re-renders. By stabilizing the function reference, useCallback allows React to recognize unchanged props and skip those renders.
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:
Appre-renders- a new
onSelectfunction is created - each
ListItemreceives 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:
handleSelectstays the same across rendersListItemprops donât changeReact.memocan 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.