Skip to content

useCallback

Every render creates new function instances. () => deleteIncome(id) on render 1 and () => deleteIncome(id) on render 2 are two different function objects, even if they do the same thing. useCallback caches a function so the reference stays stable between renders.

import { useCallback } from 'react';
const addIncome = useCallback((source) => {
setMonthData(d => ({
...d,
incomeSources: [...d.incomeSources, { ...source, id: crypto.randomUUID() }],
}));
}, [setMonthData]);

addIncome is the same function object on every render unless setMonthData changes (it never does — state setters are stable). Without useCallback, addIncome is a new function on every render.

A new function reference on every render causes two specific issues:

1. React.memo components re-render unnecessarily. If you pass a new function as a prop to a memoized child, the child sees a prop change and re-renders — even though the function does the same thing.

2. useEffect dependencies are triggered unnecessarily. If you put a function in a useEffect dependency array, a new function instance on every render would re-run the effect on every render.

If neither of these applies, useCallback provides no benefit.

useCallback(fn, deps) is exactly useMemo(() => fn, deps). They both cache something across renders. useMemo caches a value; useCallback caches a function. Internally, they are the same mechanism.

In useBudget.js, all action functions are wrapped in useCallback because they are passed as props to memoized child components:

const addIncome = useCallback((source) => {
setMonthData(d => ({
...d,
incomeSources: [...d.incomeSources, { ...source, id: crypto.randomUUID() }],
}));
}, [setMonthData]);
const deleteIncome = useCallback((id) => {
setMonthData(d => ({
...d,
incomeSources: d.incomeSources.filter(s => s.id !== id),
}));
}, [setMonthData]);

Both use the functional update form (d => ...) so they do not need to read the current state value — no extra dependencies.

useCallback has the same trade-off: it allocates memory and runs comparisons on every render. For functions not passed to memoized children or used in effect dependencies, it adds overhead without benefit.

Apply it where you can prove the benefit — a memoized child that re-renders too often, or an effect that fires too frequently. Do not apply it universally to all handlers as a default.

  1. In your useBudget hook (or App.jsx), wrap addIncome and deleteIncome with useCallback.
  2. Use the functional setter form inside so the callbacks do not depend on current state values.
  3. Add a console.log inside IncomeSection at the top of the function body. With and without useCallback on the passed functions, observe whether the child re-renders when unrelated state changes.
  4. (Requires React.memo from the next lesson — come back to this after.)
  • useCallback caches a function so its reference stays stable between renders.
  • Unstable function references cause memoized children to re-render and effects to re-fire.
  • Only use useCallback when the function is passed to a React.memo component or used as an effect dependency.
  • Use the functional setter form to keep useCallback dependencies minimal.
  • useCallback(fn, deps) is shorthand for useMemo(() => fn, deps).