Junior PRs in my reviews often wrap every function in useCallback and every array in useMemo because a blog post said so. The React Profiler showed the opposite: more memoisation, more comparison work, slower interactions. This usecallback vs usememo guide is profiler-first — what each hook does, when it helps, and the over-memoising mistake I made on a real table.
What each hook actually does
useMemo caches a computed value between renders when dependencies are unchanged. useCallback caches a function reference — it is useMemo for functions.
import { useMemo, useCallback } from "react";
function Dashboard({ rows }: { rows: Row[] }) {
const total = useMemo(() => rows.reduce((sum, r) => sum + r.amount, 0), [rows]);
const handleExport = useCallback(() => {
downloadCsv(rows);
}, [rows]);
return <Toolbar total={total} onExport={handleExport} />;
}
Neither hook prevents re-renders by itself. They only help when a child skips render because props are referentially equal — usually with React.memo.
A common confusion: developers think useMemo stops the parent re-rendering. It does not. The parent still runs its function body every time state changes — useMemo only skips recomputing one expression inside that run. useCallback is the same for function identity. If your performance problem is "the whole page re-renders on every keystroke," move state down or split context before touching memo hooks.
The over-memoising mistake I made
On a 500-row admin table I wrapped every cell formatter in useMemo and passed useCallback handlers to each row. Scroll jank got worse — dependency checks on every scroll event dominated.
// BEFORE — memo soup, slower scroll
const formatted = useMemo(
() => rows.map((r) => formatCurrency(r.amount)),
[rows]
);
const onRowClick = useCallback((id: string) => navigate("/row/" + id), []);
// AFTER — virtualise the list, drop per-row memo
import { useVirtualizer } from "@tanstack/react-virtual";
// Only memo expensive derived data used by memoised children
Fix was virtualisation + smaller components, not more hooks. Lesson: measure before memoising.
// BEFORE — unstable inline object recreates every parent render
<HeavyChart config={{ theme: "dark", showGrid: true }} />
// AFTER — stable reference only if HeavyChart is memoised
const config = useMemo(() => ({ theme: "dark", showGrid: true }), []);
<HeavyChart config={config} />
Without React.memo on HeavyChart, the AFTER snippet still re-renders the chart — you paid for useMemo and got nothing. That is the over-memoising pattern: hooks without a memoised consumer.
How I use the React Profiler
Chrome React DevTools → Profiler → record interaction → look for components with long render times and high render counts. Yellow bars mean wasted renders. I fix structure first (split context, move state down, virtual lists), then add memo where a memoised child still re-renders with stable props.
// Wrap only when Profiler shows child re-renders are expensive
import { memo } from "react";
const HeavyChart = memo(function HeavyChart({ data }: { data: Point[] }) {
// expensive canvas draw
return <canvas />;
});
// Parent must pass stable data reference
const data = useMemo(() => computePoints(raw), [raw]);
return <HeavyChart data={data} />;
On marketing sites built with Server Components, much of the tree never hydrates — memo hooks on the server branch are pointless. See RSC vs client components.
When to use which — table
| Situation | useMemo | useCallback |
|---|---|---|
| Expensive calculation | Yes | No |
| Stable prop to memo child | For objects/arrays | For functions |
| useEffect dependency | Rarely needed | Sometimes |
| Primitive props to memo child | No | No |
| List without virtualisation | Fix list first | Fix list first |
| Context provider value | Often yes (object) | Callbacks inside value |
useCallback — legitimate use with memoised child
"use client";
import { memo, useCallback, useState } from "react";
const SearchInput = memo(function SearchInput({
onSearch,
}: {
onSearch: (q: string) => void;
}) {
console.log("SearchInput render");
return <input onChange={(e) => onSearch(e.target.value)} />;
});
export function FilterBar() {
const [query, setQuery] = useState("");
const onSearch = useCallback((q: string) => setQuery(q), []);
return <SearchInput onSearch={onSearch} />;
}
Without useCallback, SearchInput re-renders whenever FilterBar state unrelated to search changes — only worth fixing if Profiler proves SearchInput is costly.
useMemo — filtering large lists
const filtered = useMemo(
() => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
[products, query]
);
If the filter runs in under a millisecond on your data size, skip useMemo — the hook overhead can cost more than the loop.
Rule of thumb I teach in reviews
Default: no memo hooks. Add when Profiler shows a problem. useMemo for expensive derived data consumed by memoised children. useCallback for stable handlers passed to those children. Never memoise to silence ESLint exhaustive-deps without understanding why deps change.
React 19 and the future compiler may auto-memoise — until then, stay boring. Performance wins on public sites still come from server rendering and smaller bundles — Next.js performance guide.
My production setup
In production I memoise chart components and heavy derived selectors in dashboards. Marketing pages: almost zero useCallback/useMemo. At my day job I comment why when memo is added — "Profiler: Chart 40ms → 8ms" — so the next dev does not delete it blindly.
When mentoring in Bengaluru, I ask juniors to screenshot Profiler flamegraphs in PRs that add memo hooks — not because I love process, but because it proves they measured. Copy-pasting useCallback from Stack Overflow without a memo child is the most common wasted line I delete in review.
The single takeaway
useCallback stabilises functions; useMemo stabilises values. Both only matter when referential equality blocks wasted work. Profile first, memo second.
Related: React 19 features. Contact.
If this helped you
I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:
- → Buy me a coffee at buymeacoffee.com/safdarali
- 👉 Subscribe to my YouTube channel — it's free; 70+ React & Next.js tutorials
Related reading
More guides on safdarali.in — same author, production-focused.
-
How to Build a Frontend Developer Portfolio That Stands Out
Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.
May 2026 · Read article →
-
React Server Components vs Client Components — When to Use Which
Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.
May 2026 · Read article →
Top comments (1)
This is the version of the useMemo/useCallback conversation I wish more teams had in code review: the 500-row admin table getting worse from formatter and handler memoization is a much better lesson than another abstract hook example. The React.memo point matters too, since a stable config object does nothing if HeavyChart still renders every time. As a founder/engineer, I'd rather see one Profiler note like "Chart 40ms to 8ms" than a codebase full of defensive memo hooks nobody can explain.