What Is the Difference Between useMemo and useCallback?

Intermediate12 min interview
Skills tested:
Stating clearly that useMemo caches a return value while useCallback caches a function referenceUnderstanding that useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)Knowing useCallback provides no re-render benefit without React.memo on the receiving childExplaining stale closure bugs from missing dependencies and cache-miss bugs from unstable object/array dependenciesApplying the correct hook to the correct problem: computation cost vs reference stability

Advertisement

🧩 Scenario

A product list component filters products based on active filters and passes a cart handler down to memoized child components. The parent re-renders frequently due to unrelated state updates. Filtering 500 products is slow, and memoized children are still re-rendering even though nothing relevant to them changed. Walk through how useMemo and useCallback solve each problem and how you know which hook applies to which.

Architecture Walkthrough

What Each Hook Actually Caches

useMemo and useCallback share the same structure: both accept a factory and a dependency array, and both recompute only when a dependency changes. The distinction is what gets stored.

useMemo runs the factory function and caches its return value. Use it when the return value is expensive to produce — filtering or sorting large arrays, aggregating data, running a regex over a large string. useCallback caches the function reference itself. It is equivalent to useMemo(() => yourFunction, deps) — a fact that clarifies the division: if you need a stable function to pass as a prop, use useCallback; if you need to memoize what a function returns, use useMemo.

Why useCallback Requires React.memo to Be Useful

useCallback exists to prevent child re-renders by keeping function prop references stable. But a component only skips a re-render when something explicitly tells React to skip it, and that mechanism is React.memo. Without React.memo on the child, React re-renders it unconditionally regardless of whether the prop reference changed. Without useCallback, React.memo compares references that always differ and re-renders anyway. useCallback without React.memo adds comparison overhead with zero benefit.

The Dependency Array Is the Most Dangerous Part

Both hooks depend on dependency array accuracy. A missing dependency causes a stale closure — the factory captures a value that no longer reflects current state. React's eslint-plugin-react-hooks catches most omissions and should be treated as a hard requirement.

The opposite error — listing too many dependencies — invalidates the cache too frequently and eliminates the benefit. Object and array dependencies are particularly dangerous: if filters is created inline on every render, Object.is sees a new reference each time and the cache never hits. Stabilizing dependencies is often as important as the memoization itself.


Key Code Explained

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

interface Product {
  id: string;
  name: string;
  category: string;
}

interface ProductCardProps {
  product: Product;
  onAdd: (id: string) => void;
}

// React.memo required — otherwise useCallback on parent adds overhead for nothing
const ProductCard = memo(function ProductCard({ product, onAdd }: ProductCardProps) {
  console.log(`Rendering: ${product.name}`);
  return (
    <div>
      <span>{product.name}</span>
      <button onClick={() => onAdd(product.id)}>Add</button>
    </div>
  );
});


function ProductList({
  allProducts,
  filters,
}: {
  allProducts: Product[];
  filters: string[];
}) {
  const [cartCount, setCartCount] = useState(0);

  // useMemo: solves a COMPUTATION problem
  // expensiveFilter runs once when filters change; skipped on cartCount updates
  const filteredProducts = useMemo(
    () => allProducts.filter((p) => filters.includes(p.category)),
    [allProducts, filters] // recompute only when these change
  );

  // useCallback: solves a REFERENCE STABILITY problem
  // dispatch from useReducer is stable by contract; safe to list as dep
  // Without useCallback: new function reference every render → ProductCard re-renders despite React.memo
  const handleAddToCart = useCallback(
    (productId: string) => {
      setCartCount((c) => c + 1);
      // addToCart(productId)...
    },
    [] // no external deps — setCartCount updater form avoids closing over state
  );

  return (
    <>
      <p>Cart: {cartCount}</p>
      {filteredProducts.map((p) => (
        <ProductCard key={p.id} product={p} onAdd={handleAddToCart} />
      ))}
    </>
  );
}


// Sorting example — the spread matters
const sortedProducts = useMemo(
  () => [...allProducts].sort((a, b) => a.name.localeCompare(b.name)),
  [allProducts] // only re-sorts when products array changes
  // [...allProducts] is required: Array.sort mutates in place
  // mutating allProducts would corrupt React's internal reference tracking
);


// The equivalence: these are identical
const stableFn = useCallback(() => doSomething(), []);
const stableFnViaUseMemo = useMemo(() => () => doSomething(), []);
// useCallback exists only for readability — the intent is clearer

The two hooks solve two entirely different problems in ProductList. useMemo solves a computation problem: expensiveFilter is slow, so cache its result and re-run only when filters changes. useCallback solves a reference-stability problem: handleAddToCart would be a new function instance on every render, causing every ProductCard (memoized) to re-render. They are not interchangeable — you cannot use useCallback to prevent an expensive filter from running, and useMemo returns a value, not a function suitable for an event handler.


Tradeoffs

HookCachesSolvesRequires pairing with
useMemoFactory return valueRedundant expensive computationNothing required
useCallbackFunction referenceUnstable function prop causing child re-rendersReact.memo on the child
NeitherNothingNothing (simplest code)N/A — correct for cheap components

What Interviewers Actually Check

  • Whether you can state without hesitation what each hook caches
  • Whether you know useCallback(fn, deps) equals useMemo(() => fn, deps) — signals deep API understanding
  • Whether you identify the useCallback + React.memo pairing requirement without being prompted
  • Whether you can explain the stale closure bug from missing deps vs the cache-miss bug from unstable object deps
  • Whether you can describe a concrete scenario: profiling, identifying the problem type, choosing the right hook

Follow-Up Questions

  1. You have a useCallback with a dependency that changes on every render because it's an object created in the parent's render body. How do you break the cycle?
  2. React's documentation says useCallback is equivalent to useMemo(() => fn, deps). Why does useCallback exist as a separate hook?
  3. A teammate adds useMemo to every derived value in a large component "for safety." What is the argument against this?
  4. You use useMemo to cache a filtered list depending on a filters object from a URL query string parsed fresh on every render. The filter still runs every time. Why, and how do you fix it?
  5. The React Profiler shows 200 components re-rendering on every keystroke in a search input. Walk through your diagnosis and fix strategy.

Common Candidate Mistakes

  • Reaching for useCallback when the problem is computation cost — that is useMemo
  • Reaching for useMemo to stabilize a function prop — useCallback is clearer and identical in effect
  • Using useCallback without React.memo on the child — adds memoization overhead for zero re-render savings
  • Passing an inline object as a dependency to either hook — Object.is reports inequality every render and the cache never hits
  • Omitting a variable the factory closes over — produces stale results that are hard to reproduce in development

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you state what each hook caches and give a one-sentence use case for each?
  • Can you explain the useCallback(fn, deps) = useMemo(() => fn, deps) equivalence?
  • Can you identify which hook solves a computation problem vs a reference-stability problem?
  • Can you describe the paired requirement: useCallback only benefits when the child has React.memo?
  • Can you describe the dependency array failure modes: missing dep (stale closure) vs unstable dep (cache never hits)?

Summary

useMemo caches the return value of an expensive factory function, recomputing only when listed dependencies change via Object.is. useCallback caches the function reference itself and is equivalent to useMemo(() => fn, deps) — it exists as a separate hook for readability. The practical rule: useMemo for computation problems (avoid re-running a slow derivation), useCallback for reference-stability problems (prevent a memoized child from re-rendering due to a new function reference). useCallback without React.memo on the receiving child is pure overhead. Both hooks share the same dependency array failure modes: a missing dependency causes a stale closure, and an unstable object or array dependency causes the cache to never hit. Profile before applying either hook, and treat them as surgical fixes rather than defaults applied to every function and derived value in a component.

Frequently Asked Questions

Is useCallback just useMemo for functions?

Yes. useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps). useCallback exists as a separate hook for readability — it makes the intent (stabilize a function reference) explicit without the extra wrapping arrow function.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement