Quick answer: useMemo caches the result of a calculation between renders, and useCallback caches the function itself, so both exist to avoid re-running expensive or identity-sensitive work when dependencies have not changed. In most components you do not need either one. Reach for them when a calculation is measurably slow, or when a value crosses a boundary that compares by identity: memoised children, context values, or effect dependency arrays.

Quick answer card explaining useMemo and useCallback in plain language.
useMemo and useCallback roadmap listing the sections in reading order.

What useMemo and useCallback actually do

Every time a component re-renders, its function body runs from top to bottom. All calculations inside it run again, and every function you define is created fresh. Most of the time that is free, because React’s reconciliation is fast and the JavaScript engine optimises trivial work away. The rendering model this rests on is the one introduced in the React tutorial, and every hook in this article builds on the state mechanics from the useState guide. useMemo and useCallback change one thing: they remember a value between renders and hand back the identical previous one when the dependencies have not changed.

// Without: a new array identity on every render
const fullNames = users.map(u => `${u.first} ${u.last}`);

// With: same array identity until users changes
const fullNames = useMemo(
  () => users.map(u => `${u.first} ${u.last}`),
  [users]
);

// useCallback is the function version of the same idea
const handleSelect = useCallback((id) => {
  setSelected(id);
}, []);

The important detail is that memoisation is not free either. React stores your value and, on every later render, walks the dependency array with Object.is to decide whether to recompute. So each call trades a small constant check against a recomputation you hope is bigger. Two hooks, one mechanism: useMemo caches what a function returns, useCallback caches the function itself, and you can implement either in terms of the other.

The cost equation: when memoisation pays off

A memoisation call pays off only when three conditions hold together: the computation is genuinely expensive, it happens on renders where the result would be identical, and the component re-renders often enough for that to matter. Break any one of the three and you have added bookkeeping for nothing.

SituationMemoise?Why
Mapping a short list of propsNoThe calculation costs less than the hook itself
Filtering 10,000 rows on every keystrokeYesMeasured work at a rate users feel
Library calls: date parsing, chart configYesThird-party setup is usually the slow part
Component renders once or twice totalNoThere is no repeat render to save
Value must keep identity for a memoised childYesCorrectness, not speed

The last row is the one people miss. An object, array or function you pass as a prop is compared by reference, so a parent that rebuilds it every render defeats React.memo on the child no matter how cheap the calculation is. Here useMemo is not an optimisation at all; it is the mechanism that lets identity-based checks work. Build the habit of measuring first with the React DevTools Profiler, then memoising what actually showed up hot.

Concept map of memoisation: render, cache, dependencies and identity.

React.memo: the third piece of the puzzle

useMemo on a value only helps if something consumes that stability. The consumer is usually React.memo, a higher-order component that skips re-rendering a child when its props compare equal. The three tools form one system: React.memo decides whether the child rerenders, and the memoised props decide whether that decision succeeds.

const Row = React.memo(function Row({ row, onSelect }) {
  return <li onClick={() => onSelect(row.id)}>{row.label}</li>;
});

function Table({ rows }) {
  const onSelect = useCallback((id) => setSel(id), []);
  // rows is already stable from the parent's side
  return <ul>{rows.map(r => <Row key={r.id} row={r} onSelect={onSelect} />)}</ul>;
}

React.memo uses a shallow comparison of props, which is why a new inline object or arrow function in the JSX defeats it instantly. Notice also that the custom comparison function argument is almost never worth writing: hand-rolled equality checks are easy to get subtly wrong, and a shallow prop compare plus correctly memoised props covers the real cases. If a large list still re-renders wholesale, the missing piece is usually an unstable key or an unmemoised prop, not a custom comparator.

useCallback, context values and event handlers

Besides feeding React.memo, stable function identities matter in two places: effect dependency arrays and context values. In an effect, a function defined in the render body changes identity every render, so listing it as a dependency would retrigger the effect constantly. Wrapping it in useCallback makes the dependency honest, which is the same stale-closure discipline described in the useEffect guide.

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState('light');
  const value = useMemo(
    () => ({ theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) }),
    [theme]
  );
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

Without the memoised value object, every state change in App rebuilds the context value and re-renders every consumer, even ones that only read a field that did not change. For high-frequency contexts the better fix is often architectural: split the hot value, such as the setter, from the cold one, such as the data, so most consumers subscribe to the small stable half. And plain event handlers passed to your own DOM elements need no useCallback at all, because no memo boundary sits between them and the element.

Measuring before you memo with the Profiler

Memoisation decisions made from intuition are usually wrong in both directions: people sprinkle hooks on cheap code and skip the one expensive calculation that actually hurts. The Profiler settles it in a minute. Record an interaction, look at which components committed, and read the flame graph for the renders that took real milliseconds.

  • Open React DevTools, pick the Profiler tab, record one interaction, and stop.
  • Sort the flame graph by actual render duration, not by component count.
  • Find the widest bar that also re-renders unnecessarily, and check its props for new identities.
  • Add memoisation at that one boundary, record again, and confirm the graph shrank.
  • If the graph did not change, remove the hooks you just added.

The measure, change, measure loop keeps the codebase honest. It also produces a written reason for each hook, which is exactly what a reviewer needs six months later when someone wonders whether a useMemo can be deleted. Answer that question the same way it was added: with a before-and-after profile, not a guess.

How the React Compiler changed the defaults

With the React Compiler enabled, React memoises component functions automatically: values and functions your code creates are cached across renders whenever their inputs did not change, which is precisely what handwritten useMemo and useCallback were doing by hand. For new codebases the practical default is now to leave them out and let the compiler insert the memoisation, adding a hook only where a manual escape hatch is genuinely needed.

The mental model does not disappear, it just moves. Expensive computations, identity-sensitive props, context values and effect dependencies still obey the same rules, and non-React code such as a plain library call inside a loop still costs what it always cost. Reading the compiler’s output once teaches the model faster than any article: watch how it hoists your function, what it chooses to cache, and where it deliberately gives up. Whatever the tooling, the discipline stays the same: know the cost, measure the render, memoise the boundary that matters. To learn React properly with a mentor, Ampersand Academy teaches web development one-to-one.

Coverage grid showing every section covered in this memoisation article.

Writing useMemo dependency lists that stay honest

One further habit makes both review and deletion tractable: keep memoised values near the component that computes them, rather than threading them through several layers of wrappers “just in case”. A memo two boundaries away from its consumer is invisible to the reader and impossible to profile accurately, because the flame graph shows the wrapper, not the work. When the expensive calculation lives directly above the component that uses its result, the dependency list is short, the profile is unambiguous, and removing the hook later, once the compiler or a refactor makes it redundant, is a one-line change instead of an archaeology project.

A memoised calculation is only as reliable as its dependency list, and the list is only honest if every value the calculation reads appears in it. Values that appear only in the callback body but not in the array produce a cache that never invalidates: the memo returns a stale result computed from an old prop, and the bug surfaces days later as a UI that occasionally shows yesterday’s data. The exhaustive-deps lint rule exists to make this mechanical, and the right way to use it is to fix the list rather than silence the warning.

Two situations break the naive list. The first is object and array dependencies: a props object recreated each render changes identity every time, so listing it invalidates the cache constantly and the hook becomes pure overhead. The fix is to list the individual fields the calculation actually consumes, which both stabilises the cache and documents what the calculation depends on. The second is functions passed as dependencies; wrapping the function in useCallback, or reading it from a ref that an effect keeps current, keeps the memo stable without hiding a real dependency behind a stale closure.

Memoisation patterns that quietly backfire

The fourth pattern is memoising across a boundary that already memoises: wrapping a child in React.memo while its parent is itself memoised with the same props, so the outer check runs, passes, and the inner check runs and passes again for the same values. In a deeply nested tree those redundant comparisons are not free, and the fix is to memoise at the outermost boundary that can stop the propagation, leaving inner components plain. One effective check is usually enough; two is bookkeeping, and three is a sign the data flow needs restructuring rather than more caching.

The first pattern is memoising everything in a hot component on reflex. Each hook carries a dependency comparison on every render, and in a component that renders hundreds of times per second with small inputs, the comparison costs approach the cost of the work being skipped. A profile that shows the memoised functions themselves as hot bars is the signal to remove them. The second pattern is memoising a value that is then immediately unpacked, spread or cloned before use, which defeats the identity stability the memo was created to provide.

The third pattern is the memo island: a single memoised component surrounded by parents that rebuild all its props each render. The island guarantees nothing, because React.memo only helps when every prop it receives survives the shallow comparison. Rather than adding more islands, the effective move is to move the unstable data source upward or restructure the component so the expensive subtree receives only stable identifiers, fetching its own details by id. That pattern, often called the id-prop pattern, converts a render storm into a cache hit and is the most reliable fix for large trees that re-render wholesale.

Memoisation discipline in large applications

Teams that adopt memoisation without a shared convention end up with a codebase where nobody dares remove a hook, because the reason it was added was never written down. The lightweight discipline that prevents this is to attach the reason to the change: a pull request that adds useMemo or React.memo should say what the profile showed before it was added, and the follow-up profile after. That record turns a defensive habit into an engineering decision, and it makes deletion safe when the surrounding code changes.

The second half of the discipline is knowing where hooks are worth reviewing at all: list rendering, context providers, and any component between a fast-changing parent and an expensive child. These boundaries carry the identity contracts that memoisation depends on, and a regression in one of them shows up as a frame-rate problem long before it shows up in a code review. With the React Compiler handling most of the mechanical work on new code, the remaining human job at these boundaries is exactly the same one: keep the data flow legible, keep identities stable where a boundary compares them, and measure before adding anything.

Frequently asked questions

Should I wrap everything in useMemo?

No. Memoisation costs a dependency check and a cache entry on every render. Use it for calculations you have measured as expensive, or for values whose identity must stay stable across renders.

What is the difference between useMemo and useCallback?

useMemo caches the value returned by a function. useCallback caches the function itself. They use the same dependency mechanism; useCallback is roughly useMemo applied to a function definition.

Do I still need these hooks with the React Compiler?

In new code, usually not: the compiler inserts equivalent memoisation automatically. Keep them for manual escape hatches and for codebases not yet compiled.

Does useMemo guarantee the function never re-runs?

No. It re-runs whenever any dependency changes, compared by Object.is. A new object, array or function identity in the dependency array counts as a change every render.

Why does my memoised child still re-render?

Almost always an unstable prop: an inline object, array or arrow function recreated each render, or a context value rebuilt without memoisation. Stabilise the prop identity and the child’s memo starts working.

Last updated on · Written by