Right, let’s talk about useCallback and useMemo. You’ve probably heard they’re for “performance,” and then you saw a dozen articles telling you to wrap everything in them. Please, for the love of all that is holy, don’t do that. They’re not magic performance dust; they’re precision tools with very specific—and honestly, somewhat niche—use cases. Misusing them can actually make your app slower and definitely more complicated. I’m here to give you the straight talk on when and why you’d actually need them.

The core concept here is memoization. It’s a fancy term for caching the result of a function call. If you call a function with the same arguments again, instead of doing the work, you just return the cached result. useMemo memoizes a value (the result of some computation), and useCallback memoizes a function itself. Under the hood, they’re essentially the same thing, which we’ll get to in a second.

The Golden Rule: Referential Equality

This is the entire reason these hooks exist. In JavaScript, two functions or objects are not equal, even if they look identical.

() => {} === () => {}; // false
{ id: 5 } === { id: 5 }; // false

React uses Object.is (a stricter version of ===) to compare things. When a component re-renders, every function inside it is re-created. Every object literal ({}) is a new object. This becomes a problem when you pass these as props to a component you’ve React.memo-ized, or when they’re dependencies of a useEffect or useCallback. The child component sees a new prop (even though its contents are the same) and thinks it needs to re-render, defeating the purpose of the memoization.

useMemo: For Expensive Calculations

useMemo caches the result of a function so you don’t have to re-run it on every render.

import { useMemo } from 'react';

function MyComponent({ items, filterTerm }) {
  // This runs on every single render, even if `items` and `filterTerm` are unchanged.
  const filteredItems = items.filter(item => item.name.includes(filterTerm));

  // This only re-runs when `items` or `filterTerm` change.
  const memoizedFilteredItems = useMemo(() => {
    return items.filter(item => item.name.includes(filterTerm));
  }, [items, filterTerm]); // dependencies array

  return <ItemList items={memoizedFilteredItems} />;
}

When to use it: When the calculation is genuinely expensive (like sorting a massive array, complex math, or data transformation). Don’t bother memoizing a simple .filter() on a ten-item array; the cost of memoization (the hook’s overhead and memory usage) is higher than just doing the filter again. Your users won’t notice you saving 0.01ms, but they will notice if you over-memoize and cause garbage collection hiccups.

useCallback: For Function Props

useCallback is just syntactic sugar for useMemo(() => fn, deps). It exists because memoizing functions is a common enough need to deserve its own hook.

Its primary purpose is to stabilize a function’s identity across renders, so that a React.memo-ized child component doesn’t re-render unnecessarily.

import { useCallback, useState } from 'react';
import ReactMemoizedChild from './ReactMemoizedChild';

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

  // ❌ Bad: This function is new on every render.
  const increment = () => setCount(c => c + 1);

  // ✅ Good: The function is memoized and stable across renders... until `setCount` changes.
  // (Spoiler: `setCount` is stable from React, so this function is stable forever).
  const memoizedIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, [setCount]); // `setCount` is stable, so deps are stable.

  return (
    <div>
      <button onClick={memoizedIncrement}>Increment</button>
      <ReactMemoizedChild onSomethingChange={memoizedIncrement} />
    </div>
  );
}

The most important pitfall: useCallback does not memoize the function’s logic. It memoizes the function instance. If your function uses a variable from the component’s scope (like a state or prop), you must include it in the dependency array, or your function will “see” a stale value from a previous render. This is the exact same rules as useEffect.

When You Absolutely Need Them

  1. Passing callbacks to optimized children: When you pass a callback to a component wrapped in React.memo.
  2. Function as a dependency: When the function is a dependency of another Hook, like a useEffect or another useCallback. A stable function identity prevents that effect from re-running on every render.
  3. Custom Hooks: When you’re writing a custom hook that returns functions, it’s often a good practice to return memoized ones so the consumer doesn’t accidentally break their own optimizations.

The Honest Truth About Overuse

The React docs themselves say you might not need these hooks everywhere. They add complexity. You now have to manage dependency arrays correctly. The performance benefits are often negligible unless you’re dealing with very expensive operations or very frequent re-renders of large component trees.

My advice? Build your app first. Profile it. Open the React DevTools profiler and see what’s actually causing slow re-renders. If you see a component re-rendering solely because its function prop identity changed, then you go back and apply useCallback. Don’t pre-optimize. You’ll save yourself a lot of headache and messy code. Consider these hooks a targeted medicine, not a daily vitamin.