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
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.


