useMemo & useCallback

Reviewed & published by Brayan K

useMemo caches a computed value and recomputes it only when a dependency changes. useCallback caches a function so its reference stays stable across renders. Both are memoization tools: they trade a little memory to skip redundant work. By the end you'll know when each helps — and when it just adds noise.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ useMemo — Cache a Computed Value

When a component re-renders, every line in its body runs again — including expensive calculations. useMemo remembers the result and recomputes only when a value in its dependency array changes.

import { useMemo } from "react";

function ProductList({ products, query }) {
  // Recomputes ONLY when products or query change — not on every render.
  const filtered = useMemo(() => {
    return products.filter(p => p.name.includes(query));
  }, [products, query]);   // 👈 dependency array

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

The demo models a cache that recomputes only when its input changes — watch the work counter:

// useMemo CACHES a computed value and only recomputes when a dependency
// changes. We model that cache here as plain JS so you can watch it skip work.

let runs = 0;
function expensiveSum(n) {
  runs++;                       // count how often we actually do the work
  let total = 0;
  for (let i = 1; i <= n; i++) total += i;
  return total;
}

// A tiny memoizer: remembers the last input and its result.
function memoize(fn) {
  let lastArg, lastResult, has = false;
  return (arg) => {
    if (has && arg === lastArg) return lastResult;   // same dep -> reuse
    lastArg = arg; lastResult = fn(arg); has = true;
    return lastResult;
  };
}

const sum = memoize(expensiveSum);
console.log(sum(1000));   // computes
console.log(sum(1000));   // cached — no recompute
console.log(sum(2000));   // dep changed -> computes again
console.log("times the work actually ran:", runs);   // 2, not 3

Your turn — complete the dependency check so a repeated call reuses the cache:

// 🎯 YOUR TURN — fill in each ___ then run it.
// Finish the memoizer's "did the dependency change?" check.

function memoize(fn) {
  let lastArg, lastResult, has = false;
  return (arg) => {
    // 1) Reuse the cached result only if we HAVE one AND arg matches lastArg.
    if (has && arg === ___) return lastResult;   // 👉 lastArg
    lastArg = arg; lastResult = fn(arg); has = true;
    return lastResult;
  };
}

let runs = 0;
const double = memoize(x => { runs++; return x * 2; });
double(5);
double(5);          // should be cached
double(5);          // should be cached
console.log("runs:", runs);

// ✅ Expected output:
//    runs: 1

2️⃣ useCallback — Stable Function References

Every render creates brand-new function objects. Usually that's fine — but if you pass a function to a memoized child, or list it in another hook's dependency array, a fresh reference each render defeats the optimization. useCallback hands back the same function until its deps change.

import { useCallback } from "react";

function SearchBar({ onSearch }) {
  // Same reference across renders unless onSearch changes.
  const handleChange = useCallback((e) => {
    onSearch(e.target.value);
  }, [onSearch]);

  return <input onChange={handleChange} />;
}

// Identity: these two are equivalent —
// useCallback(fn, deps)  ===  useMemo(() => fn, deps)

The demo shows why references matter: a fresh function is never === to another, but a memoized one is:

// useCallback keeps the SAME function reference across renders (until deps
// change). Why it matters: objects/functions are compared by reference.

function makeHandler() {
  return () => "clicked!";
}

// Without memoization: a brand-new function each "render".
const a = makeHandler();
const b = makeHandler();
console.log("new each time, equal?", a === b);   // false — different references

// With memoization (deps unchanged): the SAME reference is reused.
let cached = null;
function useStableHandler() {
  if (cached === null) cached = () => "clicked!";   // create once
  return cached;
}
const x = useStableHandler();
const y = useStableHandler();
console.log("memoized, equal?", x === y);          // true — same reference
// That stable reference is what lets a memoized child skip re-rendering.

3️⃣ When NOT to Memoize

Memoization isn't free: React stores the cached value and compares dependencies every render. For cheap computations or functions that aren't passed to memoized children, that overhead can cost more than it saves. Reach for these hooks when you have a measured performance problem, not by reflex.

// ❌ Pointless — adding two numbers is already instant.
const total = useMemo(() => a + b, [a, b]);

// ✅ Worth it — sorting/filtering thousands of rows.
const sorted = useMemo(() => bigArray.slice().sort(compare), [bigArray]);

// ✅ Worth it — a stable callback for a memoized child or an effect dep.
const onSelect = useCallback((id) => select(id), [select]);

Rule of thumb: useMemo for expensive values, useCallback for references a child or effect depends on. Everything else: leave it plain.

Reorder Challenge

These lines memoize a filtered list and a stable handler. Put them in a correct, working order:

A.   const visible = useMemo(() => items.filter(i => i.active), [items]);
B.   function List({ items, onPick }) {
C.   return <ul>{visible.map(i => <li key={i.id} onClick={() => pick(i.id)}>{i.name}</li>)}</ul>;
D.   const pick = useCallback((id) => onPick(id), [onPick]);
E.   }

Quick Recall

1) useMemo(() => a + b, [a, b]) — does memoizing this help?

No. Adding two numbers is trivial; the comparison overhead costs more than it saves. Leave it plain.

2) What does useCallback(fn, deps) equal in terms of useMemo?

useMemo(() => fn, deps). useCallback memoizes the function itself; useMemo memoizes a computed value.

3) A useCallback recreates its function every render. Most likely cause?

A dependency in its array changes every render (often an object/array literal created inline). Stabilize that dependency.

Common Errors (and the fix)

📋 Quick Reference

GoalSyntax
Cache a valueconst v = useMemo(() => compute(a), [a]);
Cache a functionconst fn = useCallback(() => act(a), [a]);
Recompute alwaysomit useMemo (just compute inline)
EquivalenceuseCallback(fn, d) === useMemo(() => fn, d)

Mini-Challenge: Count the Recomputes

Use the memoizer to wrap a function. Call it three times with the same input, once with a new input, then log how many times the real work ran. Predict the number before you run it.

function memoize(fn) {
  let lastArg, lastResult, has = false;
  return (arg) => {
    if (has && arg === lastArg) return lastResult;
    lastArg = arg; lastResult = fn(arg); has = true;
    return lastResult;
  };
}

let runs = 0;
const sq = memoize(n => { runs++; return n * n; });
// your code here: call sq(4) three times, then sq(5) once, then log runs

🎉 Lesson Complete

Practice quiz

What does useMemo memoize?

  • A function itself
  • An entire component
  • A computed value (the result of running a function)
  • A DOM node

Answer: A computed value (the result of running a function). useMemo caches a computed VALUE and only recomputes it when a dependency changes.

What does useCallback memoize?

  • The function itself (a stable reference)
  • The return value of the function
  • The component's props
  • The dependency array

Answer: The function itself (a stable reference). useCallback caches the FUNCTION itself so its reference stays stable across renders until its deps change.

useCallback(fn, deps) is equivalent to which useMemo call?

  • useMemo(fn, deps)
  • useMemo(() => fn(), deps)
  • useMemo(fn(), deps)
  • useMemo(() => fn, deps)

Answer: useMemo(() => fn, deps). useCallback(fn, deps) === useMemo(() => fn, deps): it returns the function itself rather than calling it.

Why do stable function references matter for a memoized child component?

  • Functions run faster when memoized
  • A new function reference each render defeats the child's memoization and re-renders it
  • React.memo only works with strings
  • It prevents the child from mounting

Answer: A new function reference each render defeats the child's memoization and re-renders it. Functions are compared by reference. A fresh function every render is never === to the last one, so a memoized child re-renders anyway unless you give it a stable reference.

Is it worthwhile to write useMemo(() => a + b, [a, b])?

  • No — adding two numbers is trivial and the comparison overhead costs more
  • Yes, it always speeds things up
  • Yes, because addition is expensive
  • Only in production

Answer: No — adding two numbers is trivial and the comparison overhead costs more. Memoization has a cost (storing the value, comparing deps). For trivial work like a + b it adds overhead for no benefit.

A useCallback recreates its function on every render. What is the most likely cause?

  • React is broken
  • The function is too large
  • A dependency in its array changes every render (often an inline object/array literal)
  • useCallback always recreates

Answer: A dependency in its array changes every render (often an inline object/array literal). If a value in the deps array changes every render (commonly a new object/array literal created inline), the callback is recreated. Stabilize that dependency.

What is the mistake in useMemo(compute(), [a])?

  • The deps array is wrong
  • compute() is called immediately instead of passing a function
  • useMemo needs three arguments
  • compute must be async

Answer: compute() is called immediately instead of passing a function. You must pass a function: useMemo(() => compute(), [a]). Writing compute() runs it immediately every render, defeating memoization.

Do useMemo and useCallback prevent re-renders by themselves?

  • Yes, they stop all re-renders
  • Yes, but only for class components
  • No, they have no effect on rendering
  • No — they give stable references; React.memo on the child uses them to skip its render

Answer: No — they give stable references; React.memo on the child uses them to skip its render. These hooks provide stable references/values. React.memo on the child uses those stable references to skip its own re-render — they work together.

Which is a good use case for useMemo?

  • Adding two numbers
  • Sorting or filtering thousands of rows
  • Reading a constant string
  • Logging to the console

Answer: Sorting or filtering thousands of rows. useMemo is worth it for genuinely expensive computations like sorting/filtering large arrays, not trivial operations.

What is wrong with useMemo(fn, []) when fn reads a prop value?

  • Nothing, it is ideal
  • It recomputes too often
  • It never refreshes, so it keeps a stale value when the prop changes
  • Empty arrays are not allowed

Answer: It never refreshes, so it keeps a stale value when the prop changes. An empty deps array never changes, so the cached value never refreshes even when the value it reads changes. Include every value the computation reads.

Continue this course

Related lessons