Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
useCallback memoizes a function so that the same function reference is returned across renders as long as its dependencies don't change. The reason this matters: React.memo on a child component does a shallow comparison of props, and a new function created on every render is always a different reference even if it does the same thing. Without useCallback, memoizing a child component is useless the moment you pass it any callback prop. The flip side: useCallback has its own overhead, and memoizing a function whose parent isn't even memoized wastes that overhead entirely.
A function defined inside a component body is a new object every render. useCallback returns the same object as long as its deps haven't changed — critical for preventing React.memo from bailing out.
import { useState, useCallback, memo } from 'react';
// Child only re-renders when its props change
const ExpensiveChild = memo(({ onAction }: { onAction: () => void }) => {
console.log('child rendered');
return <button onClick={onAction}>Act</button>;
});
// WITHOUT useCallback: child re-renders on every parent render
export function ParentBad() {
const [count, setCount] = useState(0);
// New function reference every render → memo is useless
const handleAction = () => console.log('action');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Parent count: {count}</button>
<ExpensiveChild onAction={handleAction} />
</div>
);
}
// WITH useCallback: child skips re-render when parent count changes
export function ParentGood() {
const [count, setCount] = useState(0);
// Stable reference — deps array is empty, never changes
const handleAction = useCallback(() => console.log('action'), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Parent count: {count}</button>
<ExpensiveChild onAction={handleAction} />
</div>
);
}memo() from ExpensiveChild but keep useCallback in ParentGood. Increment the counter and observe that the child still re-renders. This confirms that useCallback alone does nothing — it only has value when paired with memo (or used as a useEffect dependency).handleAction to depend on count: useCallback(() => console.log('action', count), [count]). Now click the counter — the child should re-render every time because count is a dependency and changes. This shows the dependency cost of useCallback.Use these three in order. Each builds on the one before.
In one paragraph, explain why useCallback alone doesn't prevent child re-renders — and what else must be in place for it to have any effect.
Walk me through what happens in memory when useCallback re-uses vs. replaces a function reference. How does React.memo use this reference for its comparison, and why does Object.is matter here?
Many codebases over-apply useCallback to every function in every component. Describe a profiling methodology for determining whether useCallback is helping or hurting performance in a specific component tree.