Passing callback functions
Extra re-renders from callbacks happen when a child component is wrapped in React.memo, but the function-prop reference changes on every parent render. The fix is a stable function identity plus careful memoization.
What to do
- Make callbacks stable (
useCallback)
javascript
const handleSelect = useCallback((id: string) => {
setSelected(id); // can go through a functional update
}, []); // only the dependencies you actually needA stable reference -> fewer chances to "wake up"
React.memochildren.
- Memoize the child (
React.memo)
javascript
const Item = React.memo(function Item({ onSelect, item }) {
return <button onClick={() => onSelect(item.id)}>{item.title}</button>;
});Without
React.memoon the child, a stable callback will not help - it re-renders with the parent anyway.
- Avoid function "factories" in JSX
javascript
// bad: a new function every time
{items.map(i => <Item key={i.id} onSelect={() => onSelect(i.id)} item={i} />)}
// good: one stable function + the data goes as a separate prop
const onSelect = useCallback((id: string) => { /* ... */ }, []);
{items.map(i => <Item key={i.id} onSelect={onSelect} item={i} />)}- Use functional updates so you do not have to pull state into dependencies
javascript
// bad: pulls selected into deps -> breaks stability
const onToggle = useCallback(() => setSelected(selected + 1), [selected]);
// good: the reference is stable
const onToggle = useCallback(() => setSelected(s => s + 1), []);- When you need the "latest" state but a stable reference - the useEvent pattern (through a ref)
javascript
function useEvent<T extends (...a: any[]) => any>(fn: T): T {
const ref = useRef(fn);
useLayoutEffect(() => { ref.current = fn; });
return useCallback(((...args) => ref.current(...args)) as T, []);
}
// Usage:
const onChange = useEvent((value: string) => {
// reads the freshest state/props, but the callback's reference never changes
});- Do not pass new objects with callbacks every time
javascript
// bad: options is new -> wakes up the child
<Chart options={{ onHover }} />
// good: memoize the object prop
const chartOptions = useMemo(() => ({ onHover }), [onHover]);
<Chart options={chartOptions} />- Sometimes it is simpler to change the props API
- Instead of
onSelect={() => onSelect(item.id)}, passidand create the handler itself insideItem. - Or pass a single
dispatch(fromuseReducer) - its reference is stable by contract.
- Split components and lift memoization higher
- Move the "heavy" part into a
React.memocomponent, so that even when the callback changes, only a small node re-renders.
What not to do (common pitfalls)
- Put
useCallbackeverywhere. It also costs a bit of CPU. Use it where:- the child is memoized (
React.memo/memo), and - the function actually goes to it as a prop.
- the child is memoized (
- Throw everything into the dependency array. Keep the deps array minimal and correct; for state, prefer functional updates more often.
- Use an array index as the
keyin lists with callbacks - reordering will lose the memo benefit and the children's state.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.