Skip to content
All posts
2 min read

Rethinking useMemo and useCallback

A follow-up to my earlier post: most of the time you do not need these hooks, and the React Compiler is why.

  • #react
  • #performance
  • #hooks

A while back I wrote a post explaining useMemo and useCallback. It is still true, but if I wrote it today I would lead with a caveat I left out: most of the time you should not reach for either of them. I see a lot of code, my own included, where these hooks were added out of habit rather than need. Here is how I think about them now.

The honest default is to skip them

The reflex looks like this. You write a value, you wrap it in useMemo. You write a handler, you wrap it in useCallback.

const value = useMemo(() => compute(a, b), [a, b]);
const handleClick = useCallback(() => save(id), [id]);

Most of the time neither wrapper is doing anything useful. compute(a, b) was cheap to begin with, and handleClick is being passed to a plain button that does not care whether it gets a new function each render. You have added code, a dependency array to keep in sync, and a little cognitive tax, in exchange for nothing measurable.

Memoization is not free

This is the bit people forget. useMemo and useCallback have a cost of their own. React still has to store the previous value, compare the dependency array on every render, and decide whether to reuse. For cheap work that comparison can cost more than just doing the work again. Wrapping everything does not make an app faster. It makes it busier.

So the question is never "could I memoise this". It is "have I measured a problem that this fixes". If you have not opened the profiler, you do not have your answer yet.

The React Compiler changes the maths

The bigger shift is the React Compiler. It reads your components at build time and inserts memoisation for you, at a finer grain than you would ever do by hand. When it is on, the vast majority of manual useMemo and useCallback calls become noise you can delete.

In Next.js it is a single option.

const nextConfig = {
  reactCompiler: true,
};

It is not on by default and it does add build time, since it leans on Babel, so it is a deliberate choice rather than a free win. But the direction of travel is clear. Hand memoisation is becoming the exception, not the rule.

When I still reach for them by hand

There are two cases where I still write these hooks on purpose.

The first is a genuinely expensive computation, the kind you can feel. Sorting or filtering a large list, parsing something big, work that clearly shows up when you profile.

const sorted = useMemo(() => bigList.sort(compare), [bigList]);

The second is when I need a stable reference, not for rendering but because something else depends on identity. An effect with an object in its dependency array is the classic one. Without a stable reference the effect runs every render.

const options = useMemo(() => ({ signal }), [signal]);
 
useEffect(() => {
  subscribe(options);
}, [options]);

Both of these are about a real, observable problem, not a vague sense that memoising is tidier.

The rule I use now

Write the simple version first. Measure before you optimise. Let the compiler handle the ordinary cases. Keep the manual hooks for the few places where you have profiled a real cost or you genuinely need a stable identity. That is a much shorter list than the one I used to reach for, and my components are easier to read for it.