Skip to main content

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

  1. Make callbacks stable (useCallback)
javascript
const handleSelect = useCallback((id: string) => { setSelected(id); // can go through a functional update }, []); // only the dependencies you actually need

A stable reference -> fewer chances to "wake up" React.memo children.

  1. 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.memo on the child, a stable callback will not help - it re-renders with the parent anyway.

  1. 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} />)}
  1. 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), []);
  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 });
  1. 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} />
  1. Sometimes it is simpler to change the props API
  • Instead of onSelect={() => onSelect(item.id)}, pass id and create the handler itself inside Item.
  • Or pass a single dispatch (from useReducer) - its reference is stable by contract.
  1. Split components and lift memoization higher
  • Move the "heavy" part into a React.memo component, so that even when the callback changes, only a small node re-renders.

What not to do (common pitfalls)

  • Put useCallback everywhere. It also costs a bit of CPU. Use it where:
    1. the child is memoized (React.memo/memo), and
    2. the function actually goes to it as a prop.
  • 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 key in lists with callbacks - reordering will lose the memo benefit and the children's state.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.