Skip to content
C Codeloom
React

React.memo, useMemo, and useCallback Demystified

When memoization actually helps in React: the difference between React.memo, useMemo, and useCallback, and how to avoid memoizing for no reason.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What React.memo, useMemo, useCallback actually do
  • When memoization helps vs hurts
  • How referential equality affects renders
  • How to measure before optimizing
  • Patterns that avoid the need for memoization

Prerequisites

  • Comfortable with React hooks

Memoization in React is one of the most over-applied optimizations in the ecosystem. The three tools — React.memo, useMemo, useCallback — exist for good reasons, but using them everywhere often makes code slower and harder to read. This guide explains what each one does, when it pays off, and how to tell whether you actually need it.

What and why

React.memo wraps a component and skips re-rendering it if its props are shallowly equal to the previous render. useMemo caches a computed value across renders so an expensive calculation does not run every time. useCallback caches a function reference so its identity stays stable across renders.

All three exist because React re-renders by default. When a parent renders, every child renders too unless something prevents it. Memoization prevents that re-render or the work inside it, in exchange for the cost of comparing inputs and storing the previous result.

Mental model

Picture each component as a function that runs whenever something nearby changes. Memoization is a cache around either the function call (React.memo) or a value inside it (useMemo, useCallback).

Parent renders
 |
 v
+-- compute child props
|     useMemo / useCallback cache values & functions here
|
v
Child:
 React.memo compares previous props to new props
 |
 +-- equal     -> skip render
 +-- not equal -> render normally
Where each memo lives in the render cycle

The key insight: React.memo does nothing unless the props are stable across renders. If you pass a new object or function every time, the shallow comparison fails and React.memo becomes pure overhead.

Hands-on example

A heavy child being re-rendered for no reason:

function Parent() {
  const [count, setCount] = useState(0);
  const items = [1, 2, 3];
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <HeavyList items={items} onPick={(x) => console.log(x)} />
    </>
  );
}

const HeavyList = React.memo(function HeavyList({ items, onPick }) {
  // assume this is expensive
  return items.map(i => <button key={i} onClick={() => onPick(i)}>{i}</button>);
});

Even with React.memo, HeavyList re-renders on every click. Why? items is a new array on every parent render, and onPick is a new function. Shallow comparison fails.

Fix with useMemo and useCallback:

function Parent() {
  const [count, setCount] = useState(0);
  const items = useMemo(() => [1, 2, 3], []);
  const onPick = useCallback((x: number) => console.log(x), []);
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <HeavyList items={items} onPick={onPick} />
    </>
  );
}

Now items and onPick keep the same identity, React.memo sees equal props, and HeavyList skips re-rendering on count changes.

useMemo for actual computation:

const sortedRows = useMemo(() => {
  return [...rows].sort((a, b) => a.value - b.value);
}, [rows]);

If rows is a 10,000-item array, this avoids re-sorting on every keystroke in some unrelated input.

When NOT to memoize

If a child is cheap to render, React.memo adds cost without measurable benefit. The comparison itself runs every render. Wrapping every component “just in case” can make a tree slower overall.

useMemo is not free either. It allocates a dependency array, runs an equality check, and stores a value. For a simple a + b, recomputing is faster than memoizing.

The React compiler (a recent addition) automates a lot of this work. If your project uses it, manual memoization is mostly unnecessary.

Common pitfalls

  • Wrapping a component in React.memo while still passing inline objects or functions as props. The memo never hits.
  • Adding useCallback to every function in a component out of habit. Most of those functions are passed to plain DOM elements, where identity does not matter.
  • Forgetting dependencies in the array. A stale closure inside a memoized callback is a classic bug.
  • Memoizing values that are recomputed on every render anyway because their dependencies always change.

Best practices

  • Measure first. Use the React DevTools profiler to find components that re-render unnecessarily and are actually expensive.
  • Memoize at the boundary between cheap parents and expensive children, not throughout the tree.
  • Lift state up only as far as needed. Keeping state close to where it is used reduces the surface area that triggers re-renders.
  • Prefer component composition over memoization. Passing children lets the parent re-render without forcing the children to re-render.

FAQ

Does useMemo guarantee the value will be cached? No. React may discard memoized values to free memory. Do not rely on it for correctness.

Is useCallback the same as useMemo(() => fn, deps)? Yes, almost exactly. useCallback(fn, deps) is shorthand for memoizing a function reference.

Should I memoize context values? Yes, if the provider re-renders frequently. Wrap the value in useMemo so consumers do not re-render on parent re-renders.

Does the React compiler replace these hooks? It auto-memoizes safely where it can. You may still reach for explicit hooks for edge cases, but the day-to-day need shrinks dramatically.