← Back to articles

useCallback and useMemo: when to actually use them

· 4 min read

useCallback and useMemo are two of the most misunderstood React hooks. Often used reflexively "to optimize", they can actually hurt performance if applied everywhere without thinking.

In this article, I cut through the myths and give you clear rules for when to actually reach for them.

The core problem: re-renders in React

First, you need to understand how React decides to re-render a component. By default, a component re-renders every time its parent re-renders, regardless of whether its own props changed.

parent.tsx
function Parent() {
  const [count, setCount] = useState(0);

  // This function is recreated on every render
  const handleClick = () => console.log('clicked');

  return <Child onClick={handleClick} />;
}

Every time count changes, Parent re-renders, handleClick gets a new reference, and Child receives a new function prop → Child re-renders too, even though nothing relevant to it changed.

useCallback: memoize a function

useCallback returns a memoized version of a function. The function is only recreated when its dependencies change.

use-callback-example.ts
const handleClick = useCallback(() => {
  console.log('clicked', id);
}, [id]); // Only recreated when `id` changes

When useCallback is useful

Case 1: Prop passed to a memoized component with React.memo

memoized-parent.tsx
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
  return <button onClick={onClick}>Click me</button>;
});

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

  // Without useCallback: Child re-renders every time (new reference each time)
  // With useCallback: Child only re-renders if id changed
  const handleClick = useCallback(() => {
    doSomething();
  }, []); // Stable dependencies

  return <Child onClick={handleClick} />;
}

Case 2: Dependency of a useEffect

use-data-fetcher.ts
// Problem: fetchData is recreated on every render → useEffect loops infinitely
useEffect(() => {
  fetchData(id);
}, [fetchData]); // ← fetchData changes on every render!

// Fix with useCallback
const fetchData = useCallback(async (id: string) => {
  const data = await api.get(id);
  setData(data);
}, []); // fetchData is now stable → useEffect won't loop

When useCallback does nothing

simple-component.tsx
// ❌ Useless: the component re-renders anyway
function SimpleComponent() {
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  // If this component doesn't use React.memo and no useEffect
  // depends on handleClick, useCallback adds zero value
  return <button onClick={handleClick}>+</button>;
}

useMemo: memoize a computed value

useMemo memoizes the result of a computation. It only recomputes when its dependencies change.

use-memo-example.ts
const expensiveResult = useMemo(() => {
  return data.filter(item => item.active).sort((a, b) => a.score - b.score);
}, [data]);

When useMemo is useful

Case 1: Expensive computation on a large list

product-list.tsx
// ✅ Worth it if `items` has thousands of entries
const filteredItems = useMemo(() => {
  return items
    .filter(item => item.category === selectedCategory)
    .sort((a, b) => b.date - a.date);
}, [items, selectedCategory]);

Case 2: Object passed as prop to a memoized component

dashboard.tsx
// ✅ Without useMemo, config is a new object on every render
const config = useMemo(() => ({
  theme: 'dark',
  locale: locale,
}), [locale]);

return <MemoizedChart config={config} />;

When useMemo is counterproductive

counter.tsx
// ❌ Useless: the computation is trivial
const doubled = useMemo(() => count * 2, [count]);

// Just write:
const doubled = count * 2;

Memoization itself has a cost (storing the value, comparing dependencies). For simple computations, the overhead exceeds the benefit.

The golden rule

Here's how I decide whether to use these hooks:

  1. Profile first with React DevTools. Never optimize blindly.

  2. useCallback: only if the function is passed as a prop to a React.memo component, or if it's in a useEffect dependency array.

  3. useMemo: only if the computation is measurably slow, or if the value is an object/array passed to a memoized component.

Conclusion

The temptation to wrap every function in useCallback and every computation in useMemo is real, it feels like writing "clean, optimized code". In practice, it's often the opposite.

React is already very fast. Trust the rendering engine, profile before optimizing, and only reach for these hooks when you have a measurable reason to do so.