Blog

useMemo vs useCallback: the difference and when to use each

What each Hook caches, and how to tell whether memoizing a value changes anything, with or without React Compiler.

Alex Mercer

useMemo caches the value a function returns, and useCallback caches the function itself, so useCallback(fn, deps) does the same job as useMemo(() => fn, deps). Use either one where something compares that value by identity (a component wrapped in memo, the components that read a context, another Hook's dependency array) or, for useMemo, where a calculation is slow enough to measure.

In pull requests, cubic's review rule Prevent Unnecessary Re-renders from Unstable Values in React flags those values when they get a new identity on every render. It sits with the other React code review standards, in a set of code review standards grouped by language and framework.

The difference between useMemo and useCallback

Both Hooks take a function and a dependency array. On each render, React compares every dependency with its previous value using Object.is, and if none changed, the Hook returns what it cached last time. They differ in what they cache.


useMemo

useCallback

Caches

The value your function returns

The function itself

Runs your function

During render, on the first render and when a dependency changed

Never; your code calls it later

Typical use

A sorted list, a derived object, a context value

An event handler or a callback prop

Signature

useMemo(calculateValue, dependencies)

useCallback(fn, dependencies)

In this component, sortOrders stands in for any helper that returns a new sorted array:

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

const Table = memo(function Table({ rows, onSort }) {
  // ...renders one row per order and calls onSort(key) from the header
});

function OrdersTable({ orders }) {
  const [sort, setSort] = useState({ key: 'date', desc: true });

  // useMemo runs this during render and caches the array it returns
  const rows = useMemo(() => sortOrders(orders, sort), [orders, sort]);

  // useCallback caches the function itself; it runs when Table calls it
  const handleSort = useCallback((key) => {
    setSort((prev) => ({ key, desc: prev.key === key ? !prev.desc : true }));
  }, []);

  return <Table rows={rows} onSort={handleSort} />;
}
import { memo, useCallback, useMemo, useState } from 'react';

const Table = memo(function Table({ rows, onSort }) {
  // ...renders one row per order and calls onSort(key) from the header
});

function OrdersTable({ orders }) {
  const [sort, setSort] = useState({ key: 'date', desc: true });

  // useMemo runs this during render and caches the array it returns
  const rows = useMemo(() => sortOrders(orders, sort), [orders, sort]);

  // useCallback caches the function itself; it runs when Table calls it
  const handleSort = useCallback((key) => {
    setSort((prev) => ({ key, desc: prev.key === key ? !prev.desc : true }));
  }, []);

  return <Table rows={rows} onSort={handleSort} />;
}
import { memo, useCallback, useMemo, useState } from 'react';

const Table = memo(function Table({ rows, onSort }) {
  // ...renders one row per order and calls onSort(key) from the header
});

function OrdersTable({ orders }) {
  const [sort, setSort] = useState({ key: 'date', desc: true });

  // useMemo runs this during render and caches the array it returns
  const rows = useMemo(() => sortOrders(orders, sort), [orders, sort]);

  // useCallback caches the function itself; it runs when Table calls it
  const handleSort = useCallback((key) => {
    setSort((prev) => ({ key, desc: prev.key === key ? !prev.desc : true }));
  }, []);

  return <Table rows={rows} onSort={handleSort} />;
}

handleSort can list no dependencies because it passes an updater to setSort and never reads sort. React's useCallback reference shows useCallback as a thin layer over useMemo:

// Simplified from React's docs
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
// Simplified from React's docs
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
// Simplified from React's docs
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}

In React's words, the only benefit of useCallback is that it "lets you avoid writing an extra nested function inside."

When to use useMemo and useCallback

Every render runs the component body again, so an object literal, an array literal or an inline function is a new value each time, and Object.is({}, {}) is false. Wherever React compares a value with the last one, a new object counts as a change even when its contents are the same.

Context provider values

When a provider's value changes, React re-renders every component that reads that context, and wrapping those components in memo doesn't stop it. A value written as an object literal changes on every render of the provider:

type CartItem = { id: string; price: number };
type Cart = { items: CartItem[]; addItem: (item: CartItem) => void };

export const CartContext = createContext<Cart | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = (item: CartItem) => setItems((prev) => [...prev, item]);

  // A new object, holding a new addItem, every time CartProvider renders
  return <CartContext value={{ items, addItem }}>{children}</CartContext>;
}
type CartItem = { id: string; price: number };
type Cart = { items: CartItem[]; addItem: (item: CartItem) => void };

export const CartContext = createContext<Cart | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = (item: CartItem) => setItems((prev) => [...prev, item]);

  // A new object, holding a new addItem, every time CartProvider renders
  return <CartContext value={{ items, addItem }}>{children}</CartContext>;
}
type CartItem = { id: string; price: number };
type Cart = { items: CartItem[]; addItem: (item: CartItem) => void };

export const CartContext = createContext<Cart | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = (item: CartItem) => setItems((prev) => [...prev, item]);

  // A new object, holding a new addItem, every time CartProvider renders
  return <CartContext value={{ items, addItem }}>{children}</CartContext>;
}

When CartProvider re-renders for a reason that has nothing to do with the cart, every reader of CartContext renders again with the same items. Caching the function and the object fixes it:

import { createContext, useCallback, useMemo, useState, type ReactNode } from 'react';

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = useCallback((item: CartItem) => {
    setItems((prev) => [...prev, item]);
  }, []);
  const cart = useMemo(() => ({ items, addItem }), [items, addItem]);

  return <CartContext value={cart}>{children}</CartContext>;
}
import { createContext, useCallback, useMemo, useState, type ReactNode } from 'react';

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = useCallback((item: CartItem) => {
    setItems((prev) => [...prev, item]);
  }, []);
  const cart = useMemo(() => ({ items, addItem }), [items, addItem]);

  return <CartContext value={cart}>{children}</CartContext>;
}
import { createContext, useCallback, useMemo, useState, type ReactNode } from 'react';

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);
  const addItem = useCallback((item: CartItem) => {
    setItems((prev) => [...prev, item]);
  }, []);
  const cart = useMemo(() => ({ items, addItem }), [items, addItem]);

  return <CartContext value={cart}>{children}</CartContext>;
}

Rendering <CartContext value> as a provider needs React 19. On React 18, write <CartContext.Provider value>.

The useContext reference says this extra rendering is not a problem in smaller apps, and the rule leaves alone primitive values and providers near the leaves with a handful of consumers. It targets providers that much of the tree reads, such as auth, theme, app state and data clients.

useMemo can't help when part of the value changes all the time. If search-box text or a pointer position sits in the same context as the signed-in user, every change produces a new value and re-renders every consumer. The rule's fix is to split the context so the fast-changing part reaches only the components that read it.

Props passed to a React.memo component

memo lets React skip re-rendering a component when each prop is Object.is-equal to its previous value. One prop that is new on every render makes that check fail, so the component pays for the comparison and re-renders anyway.

type Filters = { category: string; inStock: boolean };
type GridProps = { filters: Filters; onSelect: (id: string) => void };

const ProductGrid = memo(function ProductGrid({ filters, onSelect }: GridProps) {
  // ...renders hundreds of product cards
});

function CatalogPage({ category }: { category: string }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // Selecting a product re-renders CatalogPage, and both props are new each time
  return (
    <>
      <ProductGrid
        filters={{ category, inStock: true }}
        onSelect={(id) => setSelectedId(id)}
      />
      <ProductDetails id={selectedId} />
    </>
  );
}
type Filters = { category: string; inStock: boolean };
type GridProps = { filters: Filters; onSelect: (id: string) => void };

const ProductGrid = memo(function ProductGrid({ filters, onSelect }: GridProps) {
  // ...renders hundreds of product cards
});

function CatalogPage({ category }: { category: string }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // Selecting a product re-renders CatalogPage, and both props are new each time
  return (
    <>
      <ProductGrid
        filters={{ category, inStock: true }}
        onSelect={(id) => setSelectedId(id)}
      />
      <ProductDetails id={selectedId} />
    </>
  );
}
type Filters = { category: string; inStock: boolean };
type GridProps = { filters: Filters; onSelect: (id: string) => void };

const ProductGrid = memo(function ProductGrid({ filters, onSelect }: GridProps) {
  // ...renders hundreds of product cards
});

function CatalogPage({ category }: { category: string }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // Selecting a product re-renders CatalogPage, and both props are new each time
  return (
    <>
      <ProductGrid
        filters={{ category, inStock: true }}
        onSelect={(id) => setSelectedId(id)}
      />
      <ProductDetails id={selectedId} />
    </>
  );
}

Every click re-renders the whole grid, though only ProductDetails needed the new id. With stable props, the grid skips:

const filters = useMemo(() => ({ category, inStock: true }), [category]);

return (
  <>
    <ProductGrid filters={filters} onSelect={setSelectedId} />
    <ProductDetails id={selectedId} />
  </>
);
const filters = useMemo(() => ({ category, inStock: true }), [category]);

return (
  <>
    <ProductGrid filters={filters} onSelect={setSelectedId} />
    <ProductDetails id={selectedId} />
  </>
);
const filters = useMemo(() => ({ category, inStock: true }), [category]);

return (
  <>
    <ProductGrid filters={filters} onSelect={setSelectedId} />
    <ProductDetails id={selectedId} />
  </>
);

setSelectedId needs no useCallback, because a state setter already has a stable identity. When you own the child, React's docs suggest passing primitives instead. A ProductGrid that takes category and inStock as separate props needs no useMemo at all.

An inline arrow around a cached function undoes the cache. React's compiler introduction points out this case: even when handleClick is wrapped in useCallback, an inline () => handleClick(item) is a new function on every render, so the memoized child always gets a new onClick.

type TaskListProps = { tasks: Task[]; onPick: (id: string) => void };

function TaskList({ tasks, onPick }: TaskListProps) {
  const handleClick = useCallback((task: Task) => onPick(task.id), [onPick]);

  // Every arrow below is new on each render, so TaskRow's memo never skips
  return tasks.map((task) => (
    <TaskRow key={task.id} task={task} onClick={() => handleClick(task)} />
  ));
}
type TaskListProps = { tasks: Task[]; onPick: (id: string) => void };

function TaskList({ tasks, onPick }: TaskListProps) {
  const handleClick = useCallback((task: Task) => onPick(task.id), [onPick]);

  // Every arrow below is new on each render, so TaskRow's memo never skips
  return tasks.map((task) => (
    <TaskRow key={task.id} task={task} onClick={() => handleClick(task)} />
  ));
}
type TaskListProps = { tasks: Task[]; onPick: (id: string) => void };

function TaskList({ tasks, onPick }: TaskListProps) {
  const handleClick = useCallback((task: Task) => onPick(task.id), [onPick]);

  // Every arrow below is new on each render, so TaskRow's memo never skips
  return tasks.map((task) => (
    <TaskRow key={task.id} task={task} onClick={() => handleClick(task)} />
  ));
}

Pass the cached function itself, onClick={handleClick}, and let TaskRow call onClick(task) with the task it already has.

Inline objects and arrows passed to DOM elements, or to components that aren't memoized and are cheap to render, are fine, and the rule doesn't flag them. memo also covers props only: a memoized component still re-renders when its own state changes or when a context it reads changes.

Functions in effect dependencies

An effect runs again whenever one of its dependencies changes identity. Inside a single component, the cleaner fix is to create the object or function inside the effect, which our React useEffect guide walks through. A function that comes from a parent can't be moved there, which is the case the rule describes: "A callback recreated in a parent and listed in a child's effect dependencies reruns that effect, and any fetch or subscription in it, on every parent render."

function SearchPage({ query }: { query: string }) {
  // A new function every time SearchPage renders
  const loadResults = () => searchProducts(query);
  return <Results load={loadResults} />;
}

function Results({ load }: { load: () => Promise<Product[]> }) {
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    let ignore = false;
    load().then((data) => {
      if (!ignore) setProducts(data);
    });
    return () => {
      ignore = true;
    };
  }, [load]); // refetches after every SearchPage render

  return <ProductList products={products} />;
}
function SearchPage({ query }: { query: string }) {
  // A new function every time SearchPage renders
  const loadResults = () => searchProducts(query);
  return <Results load={loadResults} />;
}

function Results({ load }: { load: () => Promise<Product[]> }) {
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    let ignore = false;
    load().then((data) => {
      if (!ignore) setProducts(data);
    });
    return () => {
      ignore = true;
    };
  }, [load]); // refetches after every SearchPage render

  return <ProductList products={products} />;
}
function SearchPage({ query }: { query: string }) {
  // A new function every time SearchPage renders
  const loadResults = () => searchProducts(query);
  return <Results load={loadResults} />;
}

function Results({ load }: { load: () => Promise<Product[]> }) {
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    let ignore = false;
    load().then((data) => {
      if (!ignore) setProducts(data);
    });
    return () => {
      ignore = true;
    };
  }, [load]); // refetches after every SearchPage render

  return <ProductList products={products} />;
}

Caching the function with [query] makes Results refetch only when the query changes:

const loadResults = useCallback(() => searchProducts(query), [query]);
const loadResults = useCallback(() => searchProducts(query), [query]);
const loadResults = useCallback(() => searchProducts(query), [query]);

Passing query down is simpler still. Results can call searchProducts(query) inside its effect, list [query] as the dependency, and nothing needs memoizing.

If running the effect twice would break something, memoization won't protect you. The useMemo reference calls it "performance optimization, not a semantic guarantee," and useCallback has the same caveat: React may throw a cached value away, for example when a component suspends during its first mount, and the effect then fires again.

React also recommends wrapping the functions a custom Hook returns in useCallback, so the components that call it can optimize their own code when they need to.

Expensive calculations

This case belongs to useMemo alone. React's docs say a calculation probably isn't expensive unless you're creating or looping over thousands of objects, and suggest timing it during the interaction you care about:

console.time('sort orders');
const rows = sortOrders(orders, sort);
console.timeEnd('sort orders');
console.time('sort orders');
const rows = sortOrders(orders, sort);
console.timeEnd('sort orders');
console.time('sort orders');
const rows = sortOrders(orders, sort);
console.timeEnd('sort orders');

If the logged time adds up to about 1 ms or more, memoizing it might make sense. Test with CPU throttling in a production build, since your machine is probably faster than your users' devices and development mode skews the timings. useMemo won't make the first render faster either; it only skips work on later renders.

useMemo and useCallback with React Compiler

React Compiler is a build-time tool that adds memoization to components and hooks for you. Its first stable release, React Compiler 1.0, came out on October 7, 2025. The reference pages for useMemo, useCallback and memo now open with a note that the compiler reduces the need to call them by hand, and the memo page says that with the compiler enabled, "you typically don't need React.memo anymore."

React's advice differs for new and existing code. In new code, rely on the compiler and use useMemo or useCallback where you need precise control, commonly a value used as an effect dependency that shouldn't re-fire the effect when nothing meaningful changed. In existing code, leave manual memoization in place or test carefully before removing it, because removing it can change what the compiler outputs.

The compiler keeps the useMemo, useCallback and memo calls it finds, and compiles a component only when its own analysis matches or exceeds them. The preserve-manual-memoization rule in eslint-plugin-react-hooks flags manual memoization with missing dependencies, which keeps the compiler from following that component's data flow.

Manual memoization still matters in code the compiler doesn't optimize:

  • Codebases that haven't turned it on.

  • Functions the default compilation mode, infer, doesn't pick up: it compiles functions named like components or hooks that create JSX or call hooks, plus anything marked "use memo".

  • Components and hooks it skips because they might break the Rules of React, and any function marked "use no memo".

  • Expensive helpers shared by several components, since the compiler memoizes only components and hooks and doesn't share that cache between them.

Before commenting on memoization in a pull request, check whether the file is compiled. Look for babel-plugin-react-compiler in the build config, reactCompiler in next.config (a top-level option since Next.js 16), or experiments.reactCompiler in an Expo app.json. In React DevTools, components the compiler optimized carry a "Memo" badge with a sparkle. The re-render rule skips compiled code unless the component opts out with "use no memo".

In compiled code, also watch for logic that depends on memoization. React's compiler debugging guide names code "written to rely on memoization for correctness" as one of the main ways the compiler can break an app. It may memoize differently than the hand-written code did, which can make effects over-fire, loop forever or miss updates.

Reviewing useMemo and useCallback in a pull request

Lint can tell you a dependency array is incomplete. Whether a value needed a stable identity depends on the component that compares it, which may live in another file, so that question falls to the reviewer, in human and AI code review alike. When a diff touches useMemo, useCallback or memo:

  1. Find out whether the file is compiled. If it is, a missing useMemo isn't a finding, and a new one should say what it controls.

  2. Name the consumer that compares the value. If there isn't one, the Hook adds code and changes nothing.

  3. Read provider values and props passed to memo components for object, array and function literals, including arrows that wrap a cached callback.

  4. Check that dependency arrays are complete. The exhaustive-deps rule covers useMemo and useCallback as well as useEffect, and a missing dependency leaves an outdated value in the cache.

  5. Make sure nothing depends on the cache for correctness, such as an effect that must not run twice.

  6. Flag a useMemo with side effects or no return value. The use-memo rule catches the missing return, and side effects belong in an event handler or an effect.

  7. Ask for tests when a diff removes memoization from compiled code.

A review comment on a provider might say:

AuthProvider wraps the whole app, and value={{ user, signOut }} is a new object on every render, so every component that reads AuthContext re-renders even when user hasn't changed. Could we wrap signOut in useCallback and the value in useMemo with [user, signOut]?

And one on a memoized row:

Row is wrapped in memo, but onClick={() => handleSelect(row.id)} creates a new function for every row on each render, so memo never skips and the useCallback on handleSelect has no effect. Could Row take handleSelect and call it with its own row.id?

The rule page also lists these checks as instructions to paste into AGENTS.md, so an AI coding tool follows them while it writes code. That code still needs review: on Bug Bench's performance bugs ranking, the relative rate of performance bugs found when cubic reviews each model's code runs from 0.72 (GPT-5.6 Sol) to 1.96 (GPT-6.1 Sol), where 1.00 is the all-model rate.

FAQ

Is useMemo still used now that React Compiler exists?

Yes. React calls the compiler "an optional addition to React today," so code without it still needs manual memoization where identity matters, and compiled code can still use both Hooks for precise control. The compiler also works on React 17 and 18 when you set a minimum target and add the react-compiler-runtime package. React's docs add that some future features may require the compiler to work fully.

Why does my useMemo or useCallback still run on every render?

useCallback never runs your function, so if you call the cached function during render, its work runs on every render, and that work belongs in useMemo. For useMemo, first check that you passed a dependency array at all. Then log the dependencies and compare them across renders with Object.is to find the one that changes every time. In development, Strict Mode also calls your calculation twice on purpose, to surface impure code.

What is the difference between React.memo and useMemo?

memo wraps a component so React can skip re-rendering it when its props are unchanged, while useMemo caches a value inside a component. They often work together, since useMemo and useCallback keep the props of a memo component stable. If you give memo a custom comparison function, it must compare every prop, functions included, or the component can keep calling handlers that see old props and state.

Should you wrap every function in useCallback?

No. React's docs say wrapping brings no benefit when nothing compares the function's identity, and no significant harm either, but the code gets harder to read, and a single value that's new on every render breaks memoization for the whole component. useCallback also doesn't save you from creating the function: you still create one each render, and React hands back the cached one when nothing changed. If an interaction feels slow, the Profiler panel in React Developer Tools shows which components would gain the most from memoizing.

Table of contents