mosaddik.dev
All posts

React useMemo and useCallback: when you actually need them

Mosaddik Billah 4 min read

JavaScript & React

A glowing tachometer gauge on a dark car dashboard
Photo by Chris Liverani on Unsplash
On this page
  1. What happens on every render
  2. useMemo: remember a calculated value
  3. useCallback: remember a function
  4. When not to use them
  5. Find the slow part first
  6. What about React Compiler?

useMemo and useCallback are React’s tools for remembering a value or a function between renders. They are often added everywhere “for performance”, which makes code harder to read and rarely makes it faster. This guide explains what they do, the specific cases where they help, and how to tell.

What happens on every render#

When a component renders, React runs the whole function again. Every calculation runs again, and every object, array and function inside it is created new:

JSX
function ProductList({ products, query }) {
  const visible = products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()));
  const handleSelect = (id) => console.log("selected", id);
  return <List items={visible} onSelect={handleSelect} />;
}

For most components that’s completely fine. JavaScript creates objects and functions very quickly, and filtering a few hundred items takes well under a millisecond.

It becomes a problem in two situations: the calculation is genuinely slow, or a new object or function causes extra work somewhere else.

useMemo: remember a calculated value#

JSX
const visible = useMemo(
  () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
  [products, query]
);

React runs the function on the first render and stores the result. On later renders, it returns the stored result unless something in the dependency list ([products, query]) has changed.

Use it when the calculation is measurably slow: filtering or sorting thousands of rows, building chart data, parsing a large file. A quick way to check:

JSX
console.time("filter");
const visible = products.filter(/* ... */);
console.timeEnd("filter");

If it reports around a millisecond or more on a normal laptop, memoising it is worth considering. If it reports 0.05 ms, useMemo adds code without a visible benefit.

useCallback: remember a function#

JSX
const handleSelect = useCallback((id) => {
  setSelectedId(id);
}, []);

useCallback(fn, deps) is the same as useMemo(() => fn, deps): it returns the same function object until a dependency changes. It doesn’t make the function run faster. Its only purpose is to keep the function’s identity stable.

That only matters if something compares functions by identity:

  1. A child wrapped in memo. memo skips re-rendering a component when its props are the same as last time. A new onSelect function on every render counts as a changed prop, so memo never skips. With useCallback, it can.
  2. A dependency of another hook. If a function is listed in a useEffect dependency array, a new function every render makes the effect run every render.
JSX
const Row = memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

function List({ items }) {
  const [selectedId, setSelectedId] = useState(null);
  const handleSelect = useCallback((id) => setSelectedId(id), []);
  return <ul>{items.map((item) => <Row key={item.id} item={item} onSelect={handleSelect} />)}</ul>;
}

Here useCallback and memo work together: when selectedId changes, List re-renders but the rows don’t. Without memo on Row, the useCallback would do nothing useful.

When not to use them#

  • Cheap calculations. Adding numbers, formatting a date or filtering a short list doesn’t need useMemo.
  • Functions passed to plain HTML elements. <button onClick={handleClick}> doesn’t care whether handleClick is new.
  • Functions passed to components that aren’t memoised. They re-render anyway.
  • To fix a bug. If code only works because of useMemo, the real problem is usually elsewhere, such as an effect that shouldn’t exist. useEffect explained covers the common cases.

Memoising also has a cost: React has to store the value and compare dependencies on every render, and wrong dependency lists cause stale-data bugs.

Find the slow part first#

Before adding either hook, open the React Developer Tools Profiler, record the slow interaction, and look at which components took the time. Often the fix is simpler than memoisation: moving state down into the component that uses it, so less of the page re-renders, or not storing values in state that can be calculated.

What about React Compiler?#

React Compiler is a build tool from the React team that adds this kind of memoisation automatically, working out which values and functions can be reused. In a project that uses it, you rarely need to write useMemo, useCallback or memo yourself. Understanding what they do still helps when you read existing code or track down why something re-renders.

I build React interfaces for Wix dashboards and sites as part of Wix CLI apps.

Comments