</>StackKit
</>StackKit

Developer tutorials & guides

React Performance: When and How to Use useMemo and useCallback

Stop unnecessary re-renders in React. Learn exactly when useMemo and useCallback help performance — and when they're just adding complexity.

N

Nitheesh DR

Founder & Full-Stack Engineer

8 min read852 words
#react#performance#hooks#usememo#usecallback

The Re-Render Problem

Every time a React component's state or props change, it re-renders. This is usually fine — React is fast. But when a component re-renders, it also re-creates all its functions and recalculates all its values.

For expensive calculations or functions passed to child components, this can cause unnecessary work. That's where useMemo and useCallback come in.

Key rule before you read further: Don't optimize until you have a measured performance problem. Premature memoization adds complexity and can actually hurt performance.


useMemo — Cache an Expensive Calculation

useMemo memoizes the result of a function. It only recalculates when its dependencies change.

Syntax:

const memoizedValue = useMemo(() => {
  return expensiveCalculation(a, b);
}, [a, b]);

Without useMemo — recalculates on every render:

function ProductList({ products, filterText }) {
  // This runs on EVERY render, even if products and filterText haven't changed
  const filtered = products.filter(p =>
    p.name.toLowerCase().includes(filterText.toLowerCase())
  );

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

With useMemo — only recalculates when dependencies change:

function ProductList({ products, filterText }) {
  const filtered = useMemo(
    () => products.filter(p =>
      p.name.toLowerCase().includes(filterText.toLowerCase())
    ),
    [products, filterText] // only re-runs when these change
  );

  return <ul>{filtered.map(p => <li key={p.id}</li>)}</ul>;
}

When useMemo is worth it:

  • The calculation is genuinely expensive (sorting/filtering large arrays, complex math)
  • The component re-renders often with the same inputs
  • You've confirmed the calculation is a bottleneck with profiling

When useMemo is NOT worth it:

  • Simple operations like a + b or basic string manipulation
  • Arrays/objects with fewer than ~100 items
  • Components that rarely re-render

useCallback — Cache a Function Reference

useCallback memoizes a function itself (not its return value). It returns the same function instance between renders, as long as dependencies haven't changed.

Syntax:

const memoizedFn = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Why does a stable function reference matter?

In JavaScript, every time a component renders, functions defined inside it are new objects:

function Component() {
  const handleClick = () => console.log("clicked"); // new function every render
  // handleClick === handleClick from last render → false
}

If you pass this function to a child component wrapped in React.memo, the child will re-render every time anyway, because the prop reference changed.

Without useCallback:

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

  // New function every render → Child always re-renders
  const handleSubmit = (data) => {
    saveToServer(data);
  };

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onSubmit={handleSubmit} />
    </>
  );
}

With useCallback:

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

  // Same function instance as long as no dependencies change
  const handleSubmit = useCallback((data) => {
    saveToServer(data);
  }, []); // no dependencies — never changes

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onSubmit={handleSubmit} />
    </>
  );
}

const ExpensiveChild = React.memo(({ onSubmit }) => {
  console.log("ExpensiveChild rendered");
  return <form onSubmit={onSubmit}>...</form>;
});

Now clicking the count button won't re-render ExpensiveChild.


React.memo — The Missing Piece

useCallback only makes sense alongside React.memo. Without React.memo, child components always re-render when the parent does — regardless of whether props changed.

// This component only re-renders when its props actually change
const Button = React.memo(function Button({ onClick, label }) {
  console.log("Button rendered");
  return <button onClick={onClick}>{label}</button>;
});

The trio works together: React.memo + useCallback + useMemo.


Practical Example: Search with Debounce

function SearchPage({ allProducts }) {
  const [query, setQuery] = useState("");
  const [sortOrder, setSortOrder] = useState("asc");

  // Expensive: filter + sort on potentially thousands of items
  const results = useMemo(() => {
    const filtered = allProducts.filter(p =>
      p.name.toLowerCase().includes(query.toLowerCase())
    );
    return filtered.sort((a, b) =>
      sortOrder === "asc"
        ? a.price - b.price
        : b.price - a.price
    );
  }, [allProducts, query, sortOrder]);

  const handleSort = useCallback((order) => {
    setSortOrder(order);
  }, []);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SortControls onSort={handleSort} />
      <ProductGrid products={results} />
    </>
  );
}

How to Identify Real Performance Problems

Before reaching for memoization, profile first:

  1. Open Chrome DevTools → Performance tab
  2. Click record, interact with your app, stop recording
  3. Look for long frames (red bars) and what's causing them
  4. Use React DevTools Profiler tab to see which components are re-rendering and how long they take

Only add useMemo / useCallback if profiling shows actual slowness.


Quick Decision Guide

Use useMemo when:

  • Filtering or sorting large arrays (1000+ items)
  • Complex calculations that depend on props/state
  • Reference equality matters for a downstream useMemo or useEffect

Use useCallback when:

  • Passing callbacks to memoized child components (React.memo)
  • Functions are dependencies of useEffect

Use neither when:

  • The operation is cheap
  • The component renders rarely
  • You haven't profiled and confirmed a bottleneck

Conclusion

useMemo and useCallback are surgical tools for specific performance problems — not default best practices. Write clear code first, measure second, and only then add memoization where the profiler tells you to. When used correctly, they eliminate wasted renders and keep complex UIs smooth.

Tagged

#react#performance#hooks#usememo#usecallback
N

Written by

Nitheesh DR

Founder & Full-Stack Engineer

Nitheesh is a full-stack software engineer based in Tamil Nadu, India, with hands-on experience building production SaaS applications using Next.js, TypeScript, React, Node.js, and cloud infrastructure. He founded StackKit to share the practical knowledge he uses every day — not just theory, but the real-world techniques that help developers ship better software faster.