DEV Community

Cover image for React useCallback vs useMemo — When You Actually Need Them
Safdar Ali
Safdar Ali

Posted on • Originally published at safdarali.in

React useCallback vs useMemo — When You Actually Need Them

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:

More guides on safdarali.in — same author, production-focused.

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim

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.