React

You probably don’t need useCallback here

If you review enough React code, you’ll notice a pattern. Someone (most often AI) adds useMemo and useCallback almost everywhere. The code becomes harder to rea

Fotis Adamakis
Fotis Adamakis
Senior Software Engineer / Technical Writer
4 min read
January 31, 2026

If you review enough React code, you’ll notice a pattern. Someone (most often AI) adds useMemo and useCallback almost everywhere. The code becomes harder to read, and performance does not improve. Sometimes it even gets worse.

This usually happens because two things are not clear. How React re-rendering works, and what these hooks actually do.

Let me explain.

Re-renders are normal

A component re-renders when its state changes, when its props change, or when its parent renders again.

A re-render means React will run your component function again.

That’s all.-

It does not mean the DOM updates. It does not mean something is slow.

Most re-renders are cheap and expected.

The goal is not to stop re-renders. The goal is to avoid expensive work and avoid useless re-renders in expensive children.

What useMemo really does

useMemo keeps a value between renders.

That value can be anything. A number, a string, an array, an object.

There are two important points.

First, useMemo does not reduce renders. Your component still re-renders when state or props change.

Second, useMemo only helps in two cases. When the calculation is expensive, or when you need the same reference so that other components can skip work.

A common misuse of useMemo

It’s common to memoise work that is already cheap.

const fullName = useMemo(
  () => \`${user.firstName} ${user.lastName}\`,
  \[user\]
);

This adds complexity for almost no benefit. It’s usually better to write:

const fullName = \`${user.firstName} ${user.lastName}\`;

The same happens with small .map() calls. If the list is small and the UI is simple, memoising it is often just noise.

The real reason useMemo can help

The real benefit is usually about reference stability.

If you create a new object or array inside render, it is a new reference every time. That can force extra work in places that compare by reference, like React.memo or effect dependencies.

❌ Here is a pattern I see in reviews:

const Child = React.memo(function Child({
  options,
}: {
  options: { compact: boolean };
}) {
  return <div>{options.compact ? "compact" : "spacious"}</div>;
});
export function Parent() {
  const \[count, setCount\] = useState(0);
  const options = { compact: true }; // new object every render
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {count}
      </button>
      <Child options={options} />
    </>
  );
}

Child is memoised, but it still re-renders. This is because options is a new object on every render, even though compact never changes.

In this case, useMemo would make sense!

const options = useMemo(() => ({ compact: true }), \[\]);

Now the reference is stable, and React.memo can actually skip renders.

What useCallback really does

useCallback applies the same idea to functions.

It keeps the same function reference between renders until its dependencies change.

This does not prevent re-renders. The component still runs again as usual.

useCallback only matters in cases where React or your code checks whether a function has changed to decide if some other work should run.

A common misuse of useCallback

This is everywhere. 🚩

const onClick = useCallback(() => setOpen(true), \[\]);

If this is used on a button, it doesn’t change anything.

The code becomes harder to read, without a real benefit.

Things get worse when dependencies are frozen, and bugs are introduced.

const onSave = useCallback(() => {
  api.save(formState);
}, \[\]); // wrong

This captures the first formState forever. Later updates are ignored.

If you feel like you need an empty dependency array, it is often a sign that the code structure is wrong, not that the linter is annoying.

When useCallback is actually useful

There are two common cases.

One case is passing callbacks to memoised children. This only matters when those children are expensive enough that avoiding extra renders makes a difference.

const ComplexHeavyComponent = React.memo(function ComplexHeavyComponent({ onAction }: { onAction: () => void }) {
  // imagine heavy layout work, large lists, or expensive calculations here
  return <div onClick={onAction}>Heavy content</div>;
});

function Parent() {
  const \[count, setCount\] = useState(0);

  const onAction = useCallback(() => {
    setCount(c => c + 1);
  }, \[\]);

  return <ComplexHeavyComponent onAction={onAction} />;
}

Here, the child component is assumed to be expensive to render. Keeping the callback stable allows React.memo to skip unnecessary re-renders.

If this were a simple button instead, the performance benefit would be negligible, and the extra code would not be worth it.

Another case is when a callback is part of an effect dependency. In that situation, you want the effect to run only when the real inputs change.

function Search({ query }: { query: string }) {
  const fetchResults = useCallback(async () => {
    const res = await fetch(
      \`/api/search?q=${encodeURIComponent(query)}\`
    );
    return res.json();
  }, \[query\]);

  useEffect(() => {
    fetchResults();
  }, \[fetchResults\]);
  return null;
}

Here, the callback groups the logic and its dependencies in one place. The effect stays simple, and it only re-runs when the query actually changes.

The rule I use in code review

When I see useMemo or useCallback, something always smells funny.

Usages that avoid expensive work, or prevent reference changes and extra work, are rare but always a nice surprise.

Often it’s just a useless guardrail which adds mental overhead, dependency arrays to maintain and sometimes hides bugs when used carelessly.

Remember: Clarity comes first. Optimise only when you justify the need.

Fotis Adamakis

Fotis Adamakis

Senior Software Engineer / Technical Writer

Experienced software engineer writing about front end architecture, accessibility, system design, and developer productivity. Lessons from building and maintaining large-scale frontend applications, with a focus on practical patterns that make codebases easier to understand, scale, and evolve.

Barcelona, Spain