React Interview Prep
Hooks

useMemo & useCallback

Performance Hooks

LinkedIn Hook

You wrap every function in useCallback. You memoize every calculation with useMemo. Your app is now "optimized."

Except it's actually slower than before.

This is one of the most common traps in React development. Developers learn about useMemo and useCallback, and suddenly everything gets memoized — even a function that adds two numbers, even a filtered array with three items. The overhead of memoization itself becomes the performance problem.

In interviews, this is where senior candidates separate themselves. Anyone can explain what these hooks do. The real question is: when should you NOT use them? What is referential equality, and why does it matter? What does "premature optimization" actually look like in React code?

In this lesson, I break down both hooks — what they memoize, how dependency arrays control them, the referential equality problem they solve, and the premature optimization trap that catches most developers. With real code examples and clear rules for when memoization actually pays off.

If you've been sprinkling useMemo and useCallback everywhere "just in case" — this one will change how you think about React performance.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #CodingInterview #ReactHooks #useMemo #useCallback #Performance #100DaysOfCode


useMemo & useCallback thumbnail


What You'll Learn

  • What useMemo does — memoizing computed values
  • What useCallback does — memoizing function references
  • The referential equality problem and why it matters
  • How dependency arrays control cache invalidation
  • When to actually use these hooks (and when NOT to)
  • The premature optimization trap that catches most developers

The Concept — What Are useMemo and useCallback?

Analogy: The Chef and the Recipe Card

Imagine a restaurant chef (your React component) who re-renders the full menu every time a customer walks in. One dish — the slow-roasted brisket — takes 4 hours to prepare. Without any caching, the chef starts over from scratch every time, even if nothing about the brisket recipe changed.

useMemo is a recipe card with a sticky note. The chef writes down the result of the brisket recipe and attaches a note: "Valid as long as the ingredients are the same." Next time the menu re-renders, the chef checks the sticky note. Same ingredients? Serve the cached brisket. Ingredients changed? Cook it again.

useCallback is the same idea, but for the recipe itself — not the dish. Instead of caching the cooked meal (computed value), it caches the recipe card (function reference). Why would you need to cache a recipe? Because in React's kitchen, every re-render creates a brand-new copy of every recipe, even if the instructions are identical. And some sous-chefs (child components wrapped in React.memo) refuse to re-cook unless they receive a recipe they haven't seen before. A new copy of the same recipe tricks them into unnecessary work.

The catch: Caching itself has a cost. The chef needs shelf space, needs to check sticky notes, needs to compare ingredients. If the brisket only takes 2 seconds to make, the overhead of caching is worse than just making it fresh every time.

That's the premature optimization trap. Don't memoize the salad.


How useMemo Works

useMemo takes a function and a dependency array. It runs the function and caches the result. On subsequent renders, it returns the cached result unless a dependency has changed.

const memoizedValue = useMemo(() => {
  // This function runs only when dependencies change
  return expensiveComputation(a, b);
}, [a, b]);
// If a and b are the same as last render -> return cached result
// If a or b changed -> re-run the function and cache the new result

How useCallback Works

useCallback is syntactic sugar for memoizing a function reference. Instead of caching a computed value, it caches the function itself.

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

// This is equivalent to:
const memoizedFn = useMemo(() => {
  return () => doSomething(a, b);
}, [a, b]);

The only difference: useMemo caches the return value of the function. useCallback caches the function itself.


The Referential Equality Problem

This is the core reason these hooks exist. Understanding it is essential for interviews.

Code Example 1: The Problem Without Memoization

import { useState } from "react";
import { memo } from "react";

// A child component wrapped in React.memo
// React.memo skips re-rendering if props haven't changed
const ExpensiveList = memo(function ExpensiveList({ items, onItemClick }) {
  console.log("ExpensiveList rendered!");
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [query, setQuery] = useState("");
  const [items] = useState([
    { id: 1, name: "React" },
    { id: 2, name: "Vue" },
    { id: 3, name: "Angular" },
  ]);

  // This function is re-created on every render
  // Even though the logic is identical, it's a NEW function object each time
  const handleItemClick = (id) => {
    console.log("Clicked item:", id);
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {/* Every keystroke re-renders App */}
      {/* handleItemClick is a new reference every time */}
      {/* React.memo compares props: new function !== old function */}
      {/* So ExpensiveList re-renders on EVERY keystroke, even though items didn't change */}
      <ExpensiveList items={items} onItemClick={handleItemClick} />
    </div>
  );
}

// User types "R" in the search box:
// Console output:
// "ExpensiveList rendered!"   <-- unnecessary re-render!
//
// The list has nothing to do with the search query,
// but it re-renders because handleItemClick is a new function reference.

The problem: In JavaScript, () => {} !== () => {}. Two functions with identical code are different objects. React.memo uses shallow comparison (===), so it sees a "new" prop every time and re-renders.


Code Example 2: The Fix With useCallback and useMemo

import { useState, useCallback, useMemo } from "react";
import { memo } from "react";

const ExpensiveList = memo(function ExpensiveList({ items, onItemClick }) {
  console.log("ExpensiveList rendered!");
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [query, setQuery] = useState("");
  const [items] = useState([
    { id: 1, name: "React" },
    { id: 2, name: "Vue" },
    { id: 3, name: "Angular" },
  ]);

  // useCallback caches this function reference
  // It returns the SAME function object on every render (unless dependencies change)
  const handleItemClick = useCallback((id) => {
    console.log("Clicked item:", id);
  }, []); // Empty deps = never re-created

  // useMemo caches the filtered result
  // Only recalculates when query or items change
  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {/* Now React.memo works correctly: */}
      {/* handleItemClick is the same reference -> prop unchanged */}
      {/* filteredItems only changes when query/items change -> expected re-renders */}
      <ExpensiveList items={filteredItems} onItemClick={handleItemClick} />
    </div>
  );
}

// User types "R":
// Console output:
// "ExpensiveList rendered!"   <-- renders because filteredItems changed (expected)
//
// User types "R" then backspaces back to "R":
// If the filtered result is the same array reference... depends on the filter.
// But handleItemClick is ALWAYS the same reference now. That prop no longer causes re-renders.

What changed: useCallback ensures handleItemClick is the same object across renders, so React.memo doesn't see a prop change. useMemo ensures the filtered list only recalculates when its inputs change.

useMemo & useCallback visual 1


The Premature Optimization Trap

This is the section that matters most in interviews. Knowing WHEN to memoize is more important than knowing HOW.

Code Example 3: Pointless Memoization

import { useMemo, useCallback } from "react";

function UserCard({ name, age }) {
  // POINTLESS useMemo: string concatenation is trivially cheap
  const fullDisplay = useMemo(() => {
    return `${name}, age ${age}`;
  }, [name, age]);

  // POINTLESS useCallback: this function is only passed to a <button>, not a memoized child
  const handleClick = useCallback(() => {
    alert(`Hello, ${name}!`);
  }, [name]);

  // A plain <button> is NOT wrapped in React.memo
  // It re-renders with its parent regardless of prop references
  // So caching handleClick provides zero benefit
  return (
    <div>
      <p>{fullDisplay}</p>
      <button onClick={handleClick}>Greet</button>
    </div>
  );
}

// The "optimized" version is actually SLOWER because:
// 1. useMemo must store the previous deps and compare them every render
// 2. useCallback must store the function and compare deps every render
// 3. The operations being memoized (string concat, simple function) cost almost nothing
// 4. No child component benefits from referential stability
//
// Better version — just write it normally:
function UserCardSimple({ name, age }) {
  const fullDisplay = `${name}, age ${age}`;

  const handleClick = () => {
    alert(`Hello, ${name}!`);
  };

  return (
    <div>
      <p>{fullDisplay}</p>
      <button onClick={handleClick}>Greet</button>
    </div>
  );
}

When TO Use useMemo and useCallback

USE useMemo when:
  1. The computation is genuinely expensive (sorting/filtering large arrays, complex math)
  2. The result is passed as a prop to a React.memo child
  3. The result is used as a dependency in another hook (useEffect, useMemo, useCallback)

USE useCallback when:
  1. The function is passed as a prop to a React.memo child
  2. The function is used as a dependency in useEffect
  3. The function is passed to a custom hook that depends on referential stability

DO NOT USE when:
  1. The computation is trivially cheap (string concat, basic math, small array operations)
  2. The result/function is only used by plain HTML elements (not memoized children)
  3. You're "optimizing" without measuring a real performance problem
  4. Every dependency changes on every render anyway (cache is always invalidated)

useMemo & useCallback visual 2


Code Example 4: useMemo as a Dependency Guard

import { useState, useMemo, useEffect } from "react";

function SearchResults({ query, category }) {
  const [results, setResults] = useState([]);

  // Without useMemo: this object is re-created every render
  // Even if query and category haven't changed
  // This would cause the useEffect below to fire on EVERY render
  const searchParams = useMemo(() => {
    return { query, category, limit: 20 };
  }, [query, category]);

  useEffect(() => {
    // This effect depends on searchParams
    // Without useMemo, searchParams is a new object every render
    // So this fetch would fire on every render — a serious bug!
    async function fetchResults() {
      const response = await fetch(`/api/search?q=${searchParams.query}&cat=${searchParams.category}`);
      const data = await response.json();
      setResults(data);
    }
    fetchResults();
  }, [searchParams]); // Stable reference thanks to useMemo

  return (
    <ul>
      {results.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

// Without useMemo:
// Render 1: searchParams = { query: "react", category: "docs", limit: 20 }  -> fetch fires
// Render 2: searchParams = { query: "react", category: "docs", limit: 20 }  -> fetch fires AGAIN!
//   (same values, but new object reference — useEffect sees it as "changed")
//
// With useMemo:
// Render 1: searchParams created, cached -> fetch fires
// Render 2: query and category unchanged -> useMemo returns SAME object -> useEffect skips
// Render 3: query changes to "vue" -> useMemo creates new object -> fetch fires (correct!)

This is a legitimate use case. Without useMemo, the useEffect fires on every render because objects are compared by reference, not by value. This is not about performance — it is about correctness.


Common Mistakes

Mistake 1: Memoizing everything "just in case"

// WRONG: memoizing trivial operations adds overhead with no benefit
function ProfileBadge({ firstName, lastName }) {
  const fullName = useMemo(() => firstName + " " + lastName, [firstName, lastName]);
  const handleLog = useCallback(() => console.log(fullName), [fullName]);

  return <span onClick={handleLog}>{fullName}</span>;
}

// RIGHT: just write it directly
function ProfileBadge({ firstName, lastName }) {
  const fullName = firstName + " " + lastName;
  return <span onClick={() => console.log(fullName)}>{fullName}</span>;
}

// The first version allocates extra memory for memoization caches,
// runs dependency comparisons on every render, and gains nothing
// because <span> is not wrapped in React.memo.

Mistake 2: Using useCallback without React.memo on the child

// WRONG: useCallback is pointless here
function Parent() {
  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

  // ChildButton is a regular component, NOT wrapped in React.memo
  // It will re-render with Parent regardless of prop references
  return <ChildButton onClick={handleClick} />;
}

function ChildButton({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

// FIX: Either wrap ChildButton in React.memo (if re-renders are expensive)
// or remove useCallback (if they're cheap).
const ChildButton = memo(function ChildButton({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
});

Mistake 3: Forgetting a dependency and getting stale values

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState("");

  // BUG: query is NOT in the dependency array
  // This callback always closes over the initial value of query ("")
  const handleSearch = useCallback(() => {
    onSearch(query); // query is always "" — stale closure!
  }, [onSearch]); // Missing: query

  // FIX: Include query in the dependency array
  const handleSearch = useCallback(() => {
    onSearch(query);
  }, [onSearch, query]); // Now the function updates when query changes

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

Interview Questions

Q: What is the difference between useMemo and useCallback?

useMemo caches the return value of a function — it memoizes a computed result. useCallback caches the function itself — it memoizes a function reference. In fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo when you need a cached value. Use useCallback when you need a stable function reference, typically to pass to a React.memo child or as a dependency in useEffect.

Q: When should you NOT use useMemo or useCallback?

Do not use them when the computation is trivially cheap (basic math, string concatenation, small array operations), when the value or function is only used by plain HTML elements (not memoized children), when dependencies change on every render anyway (making the cache useless), or when you haven't measured an actual performance problem. Memoization has its own cost — storing previous values, comparing dependencies — and if that cost exceeds the cost of just recomputing, you've made things slower.

Q: What is the "referential equality" problem in React?

In JavaScript, two objects or functions with identical content are not equal by reference ({} !== {}, () => {} !== () => {}). React uses referential equality (===) to compare props in React.memo, dependencies in useEffect, and dependencies in useMemo/useCallback. This means a re-created object or function is treated as "changed" even if its content is the same. useMemo and useCallback solve this by returning the same reference across renders when dependencies haven't changed.

Q: Can useMemo be used for correctness, not just performance?

Yes. A common case is when a memoized value is used as a dependency in useEffect. Without useMemo, an object or array re-created on every render causes the effect to fire every render, leading to infinite loops or redundant API calls. In these cases, useMemo is not optional optimization — it is required for correct behavior.

Q: What happens if React decides to drop a memoized value?

React does not guarantee that memoized values persist forever. The docs state that React may discard cached values in the future (e.g., for offscreen components). Your code should still work correctly if useMemo recalculates — it should be a performance optimization, not a semantic guarantee. This is why useMemo should never be used for side effects; it should always be a pure computation.


Quick Reference — Cheat Sheet

+-----------------------------------+-------------------------------------------+
| Concept                           | Key Point                                 |
+-----------------------------------+-------------------------------------------+
| useMemo(() => val, [deps])        | Caches the RETURN VALUE. Recalculates     |
|                                   | only when a dependency changes.           |
+-----------------------------------+-------------------------------------------+
| useCallback(fn, [deps])           | Caches the FUNCTION ITSELF. Returns the   |
|                                   | same reference unless deps change.        |
+-----------------------------------+-------------------------------------------+
| Equivalence                       | useCallback(fn, deps) is the same as      |
|                                   | useMemo(() => fn, deps).                  |
+-----------------------------------+-------------------------------------------+
| Referential equality              | {} !== {} and () => {} !== () => {}.      |
|                                   | Memoization preserves same reference.     |
+-----------------------------------+-------------------------------------------+
| React.memo + useCallback          | useCallback is only useful when the child |
|                                   | is wrapped in React.memo. Otherwise the   |
|                                   | child re-renders with the parent anyway.  |
+-----------------------------------+-------------------------------------------+
| Dependency as guard               | useMemo can prevent useEffect from firing |
|                                   | on every render (correctness, not perf).  |
+-----------------------------------+-------------------------------------------+
| Premature optimization            | Memoization has cost. Only memoize when   |
|                                   | the saved work exceeds the overhead.      |
+-----------------------------------+-------------------------------------------+

RULE: Measure first, memoize second.
RULE: useCallback without React.memo on the child = wasted effort.
RULE: If deps change every render, the cache is always invalidated = pointless.

Previous: Lesson 3.3 — useRef — Escape Hatch -> Next: Lesson 3.5 — useContext — Avoiding Prop Drilling ->


This is Lesson 3.4 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page