Memoization On Demand

Apply `React.memo`, `useMemo`, and `useCallback` only when profiling identifies a real performance issue.

Why?  Avoids stale closures and premature optimisation overhead.

Premature optimization is a classic rabbit hole. It’s tempting to sprinkle useMemo and wrap every component in React.memo “just in case.” The cost? Extra code to read, risk of stale closures, and more memory usage—sometimes even slower renders when dependencies change. Memoization shines when a calculation is expensive or a prop‑drilling chain triggers costly re‑renders and you can prove it in React DevTools. Otherwise, the safest—and fastest—code is the simplest one.

Correct Example

import { useMemo } from "react";

type Order = { id: number; total: number; date: string };

function useOrderStats(orders: Order[]) {
  // Heavy calculation (e.g., thousands of orders) — legit candidate for useMemo
  return useMemo(() => {
    const totals = orders.map((o) => o.total);
    const sum = totals.reduce((s, t) => s + t, 0);
    return {
      count: orders.length,
      average: sum / orders.length || 0,
      max: Math.max(...totals),
    };
  }, [orders]);
}

export default function Dashboard({ orders }: { orders: Order[] }) {
  const stats = useOrderStats(orders);

  return (
    <section className="rounded-xl border p-6">
      <h2 className="text-lg font-semibold">Order Stats</h2>
      <p>Total orders: {stats.count}</p>
      <p>Average value: ${stats.average.toFixed(2)}</p>
      <p>Largest order: ${stats.max.toFixed(2)}</p>
    </section>
  );
}

Why this is good

  • Measured need: The calculation scales with data size. Developers saw a slowdown in profiling, then wrapped it in useMemo.
  • Clean deps: Only orders matter; memo stays valid until the list truly changes.

Incorrect Example

// Over‑memoization everywhere
const FancyButton = React.memo(function FancyButton(props: { label: string }) {
  console.log("FancyButton render"); // still logs on every parent re-render
  return (
    <button className="rounded bg-blue-600 px-4 py-2 text-white">
      {props.label}
    </button>
  );
});

export default function Form() {
  const [name, setName] = useState("");

  // ❌ unnecessary useCallback: setName already stable from React
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value),
    []
  );

  // ❌ useMemo wraps a trivial string concat
  const greeting = useMemo(() => `Hello, ${name}!`, [name]);

  return (
    <>
      <input value={name} onChange={handleChange} className="border p-2" />
      <p>{greeting}</p> {/* useless memoization */}
      <FancyButton label="Submit" />
      {/* memoized component still re-renders because props change? none! */}
    </>
  );
}
// Outcome: more code, zero performance gain, and potential stale-bug
// risks if dependencies are forgotten later.

Key Takeaways

  • Start with profiling: Reach for memoization only after DevTools shows a real hotspot.
  • Expensive, pure calculations only: Heavy loops, deep object transformations, or large lists—not string templates or tiny math.
  • Keep dependency arrays exact: Missing deps = stale values; extra deps = memo never hits.
  • React.memo ≠ free pass: Components inside still re‑render if shallow‑compared props change or context updates.
  • Less code, fewer bugs: Default to plain functions; optimise where it measurably matters.