Navigate
Search topics, hooks, and fundamentals
Why does your phone keep engraving a new dial button every screen refresh?
π± Theme: Phone Speed Dial
Your component is a phone with speed-dial buttons. Every render, the phone re-engraves all its buttons β even though the contacts haven't changed. Memo'd children (ContactList) compare button serial numbers, so re-engraved buttons look "new" and trigger a re-render. useCallback keeps the same button until contacts actually change.
Without useCallback, a new function is stamped out on every render. Your memo'd contact list sees a "new" dial button serial and re-renders β even though nothing actually changed.
Phone App
Screen refreshes
Each refresh creates a new handleDial function
Contact List (memo)
re-renders: 1useCallback engraves the function once and keeps the same button serial between renders. Now memo() can do its job β the contact list stays put.
Phone App
Screen refreshes
handleDial stays the same across renders
Contact List (memo)
re-renders: 1function PhoneApp() {
const [screenRefreshes, setScreenRefreshes] = useState(0);
// Same function reference across renders!
const handleDial = useCallback(() => {
console.log("Dialing...");
}, []); // Empty deps = never changes
return (
<>
<button onClick={() => setScreenRefreshes(c => c + 1)}>
Refresh Screen
</button>
{/* memo() works! handleDial is the SAME reference */}
<ContactList onDial={handleDial} />
</>
);
}
const ContactList = memo(({ onDial }) => {
// Only re-renders if onDial actually changes!
return <button onClick={onDial}>Dial</button>;
});useCallback is your phone's speed dial β it keeps the same button until the contacts actually change, so memo'd children don't re-render needlessly.
Three playgrounds to explore how speed dial works, when to reach for it, and how to keep callbacks stable even when state changes.
Three contacts, three callback strategies. Hit "Refresh" and watch which contacts re-check their list.
Stable (useCallback [])
Unstable (inline)
Dependent ([count])
Green border = rendered once. Red border = re-rendered.
// 1. Stable β never changes
const handleStable = useCallback(() => {}, []);
// 2. Unstable β new function every render
const handleUnstable = () => {};
// 3. Dependent β changes when count changes
const handleDependent = useCallback(() => {
console.log(count);
}, [count]);Why re-program speed dial every time the call count changes? Use functional setState to keep callbacks stable.
Unstable ([count] dep)
child renders: 1Stable ([] dep)
child renders: 1Green border = rendered once. Red border = re-rendered.
Click both "Increment" buttons a few times and compare child render counts.
// Unstable: recreates when count changes
const increment = useCallback(
() => setCount(count + 1),
[count] // new callback every count change
);
// Stable: functional update, no dependency
const increment = useCallback(
() => setCount(c => c + 1),
[] // stable forever
);Answer these questions to decide if useCallback is worth it.
Is the child wrapped in memo()?
Is this function in a dependency array (useEffect, useMemo)?
Is the child expensive to render?
// DO speed dial β child is memo'd
const handleDial = useCallback(() => { ... }, [contacts]);
<MemoizedContactList onDial={handleDial} />
// DO speed dial β used in dependency array
const fetchContacts = useCallback(() => { ... }, [query]);
useEffect(() => { fetchContacts(); }, [fetchContacts]);
// DON'T β no memo, no dependency array
const handleClick = () => { ... }; // inline is fineThese are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
function Parent() {
const [count, setCount] = useState(0);
// Stable reference, but child isn't memoized β useless
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return <ChildItem onClick={handleClick} />;
}
function ChildItem({ onClick }) {
return <button onClick={onClick}>Click</button>;
}function SearchBar() {
const [query, setQuery] = useState("");
// useCallback is pointless on a native <input>
const handleChange = useCallback((e) => {
setQuery(e.target.value);
}, []);
return <input onChange={handleChange} value={query} />;
}function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// New function every render
const fetchData = () => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
};
useEffect(() => {
fetchData();
}, [fetchData]); // infinite loop!
}