React
Performance
useMemo
useCallback
React Hooks
Frontend

useMemo vs useCallback vs React.memo: When to Use Each

useMemo caches a value, useCallback caches a function, React.memo skips a re-render. The difference, when each pays off, and what the React Compiler changes.

Updated
14 min read
Chamikara Nayanajith

useMemo caches the value your function returns. useCallback caches the function itself, without calling it. That is the whole difference: useCallback(fn, deps) behaves exactly like useMemo(() => fn, deps). React.memo is a different tool: it skips re-rendering a component whose props have not changed, and it is what gives a stable useCallback function something to do.

Wrapping things in useMemo and useCallback has become a reflex, and most of the time the reflex buys nothing. Each hook adds a dependency array you have to keep correct and a comparison on every render. The runtime cost is small. The maintenance cost is not, and one dependency that is new on every render quietly turns the whole thing into overhead.

The React documentation is more sceptical than the habit. The memo reference says memo only helps a component that re-renders often with the same props and is expensive to render, and the useMemo reference lists three situations where the hook helps and says there is no benefit in any other.

The three tools in one table

ToolCachesReach for it when
useMemoThe result of a calculationThe calculation is measurably expensive, or its result is a dependency somewhere else
useCallbackA function definitionThe function is passed to a memoised child or listed in a dependency array
React.memoA component's rendered outputThe component re-renders often with identical props and is expensive to render

useEffect is not on the list because it is not a cache: it runs after render to sync with something outside React. The memo hooks often exist to keep its dependencies stable.

What is the difference between useMemo and useCallback?

The difference is what you get back. useMemo calls your function during render and returns its result: a number, an array, an object. useCallback returns your function without calling it, and your code runs later, when something calls it. Both keep one cached result, the latest, until a dependency changes, comparing each dependency with Object.is.

tsx
import { useCallback, useMemo } from 'react';

type Item = { name: string; price: number };
type CartProps = { items: Item[]; save: (items: Item[]) => void };

function Cart({ items, save }: CartProps) {
  // Runs now, during render. total is a number.
  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price, 0),
    [items]
  );

  // Runs later, on click. handleSave is a function.
  const handleSave = useCallback(() => save(items), [items, save]);

  return <button onClick={handleSave}>Save ({total})</button>;
}

Both hooks are there to show the shape, not because Cart needs them: summing a cart is cheap, and a plain <button> skips no work when its handler is stable. The ProductGrid example below is a case where both pay off.

The useCallback reference describes useCallback as a simplified useMemo that returns your function instead of calling it, so these two lines produce the same stable function:

tsx
const viaCallback = useCallback(() => setCount((c) => c + 1), []);
const viaMemo = useMemo(() => () => setCount((c) => c + 1), []);

The double arrow in the second line is why useCallback exists. React implements the two hooks separately, but they behave the same, so the choice is about reading: useCallback for functions, useMemo for everything else.

What differsuseMemouseCallback
ReturnsWhatever your function returnsYour function, uncalled
Runs your functionDuring render, on mount and when a dependency changesNever. Your code runs when something calls it
Typical jobAn expensive result, or an object or array that must keep its identityA handler for a memoised child, or a function in a dependency array
Used by mistake for the otherRuns a handler during render and caches undefinedHands a function to code that expected a value

What happens if you mix them up?

You get the wrong kind of thing back: useCallback hands you a function where you wanted a value, and useMemo runs your handler during render. Only one of the two makes any noise:

tsx
import { useCallback, useMemo } from 'react';

type Item = { name: string; price: number };

export function CartTotal({ items }: { items: Item[] }) {
  // Bug: useCallback hands back the function itself, never its result.
  const total = useCallback(
    () => items.reduce((sum, item) => sum + item.price, 0),
    [items]
  );
  return <p>Total: {total}</p>;
}

export function SaveButton({ onSave }: { onSave: () => void }) {
  // Bug: useMemo calls this function during render and caches undefined.
  const handleClick = useMemo(() => {
    onSave();
  }, [onSave]);
  return <button onClick={handleClick}>Save</button>;
}

Rendered with React 19.3, CartTotal shows Total: with nothing after it, and React logs this in development:

bash
Functions are not valid as a React child. This may happen if you return Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
  <p>{Component}</p>

SaveButton is the worse bug, because it is silent. onSave fired as the button mounted, before anyone clicked (twice under StrictMode), and handleClick cached the undefined it returned. Clicking Save afterwards did nothing and logged nothing, because an onClick of undefined is valid.

TypeScript catches both before anything runs. TypeScript 5.9.3 and 7.0.2 print the same two errors:

bash
src/Mixups.tsx(11,21): error TS2322: Type '() => number' is not assignable to type 'ReactNode'.
src/Mixups.tsx(19,18): error TS2322: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.

Both bugs are type errors with line numbers attached, which is one more reason to write React components in TypeScript.

Using useMemo and useCallback together

The common real case needs both hooks at once: a memoised child that takes a derived list and a handler. The list needs useMemo, the handler needs useCallback, and the child needs React.memo:

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

type Product = { id: number; name: string; inStock: boolean };
type GridProps = { products: Product[]; onAdd: (id: number) => void };

let gridRenders = 0;

const ProductGrid = memo(function ProductGrid({ products, onAdd }: GridProps) {
  gridRenders++;
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>
          {p.name} <button onClick={() => onAdd(p.id)}>Add</button>
        </li>
      ))}
    </ul>
  );
});

export function Catalog({ all }: { all: Product[] }) {
  const [cart, setCart] = useState<number[]>([]);

  const inStock = useMemo(() => all.filter((p) => p.inStock), [all]);
  const addToCart = useCallback((id: number) => setCart((c) => [...c, id]), []);

  return (
    <>
      <p>{cart.length} in cart</p>
      <ProductGrid products={inStock} onAdd={addToCart} />
    </>
  );
}

Here is gridRenders after mounting and clicking Add three times with React 19.3, first as written and then with one piece removed each time:

bash
useMemo + useCallback + memo      grid renders: 1
useMemo only (inline onAdd)       grid renders: 4
useCallback only (inline filter)  grid renders: 4
both hooks, grid without memo     grid renders: 4

Drop either hook and one prop is new on every render, which is enough to fail the memo comparison; drop memo and there is no comparison at all. Under StrictMode in development the counts double, to 2 and 8, because React renders every component twice on purpose.

useMemo caches a value

useMemo takes a function and a dependency array, runs the function, and returns the same result on later renders until a dependency changes.

tsx
import { useMemo, useState } from 'react';

type Product = { id: number; name: string; price: number };

function ProductList({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');

  // Only re-sorts when products or query change.
  const visible = useMemo(() => {
    return products
      .filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
      .sort((a, b) => a.price - b.price);
  }, [products, query]);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <List items={visible} />
    </>
  );
}

Whether that useMemo earns its place depends on how many products there are, and on which renders it saves.

When should you use useMemo?

Use it when the calculation measures over about 1ms, or when the result has to stay the same object because an effect or a memoised child depends on it. For the first case, measure instead of guessing: wrap the calculation in a timer and repeat the interaction a few times.

tsx
console.time('filter+sort');
const visible = products.filter(/* ... */).sort(/* ... */);
console.timeEnd('filter+sort');

For the filter and sort above, over synthetic products with a query matching 40% of them, headless Chrome 152 on a desktop Core i5-7400T gave these medians, unthrottled and with CPU throttling at 4x, the same setting DevTools uses:

bash
unthrottled   4x slowdown
    40 products    0.002 ms      0.008 ms
 4,000 products      0.7 ms        3.0 ms
40,000 products      9.2 ms         40 ms

The 1ms line comes from the React docs on expensive calculations. Unthrottled, 4,000 products lands just under it; at a 4x slowdown it takes 3ms and is well over. Measured with throttling on, a calculation under 1ms is not what is making your app feel slow, and adding useMemo is cargo cult. Over 5ms it is worth memoising, because you are eating into the 16ms budget a 60fps frame gives you. Between the two, use judgement about how often the component renders.

Know which renders the memo saves, though. At 40,000 products it spares you 9ms, or 40ms throttled, on every render where neither products nor query changed, such as the parent re-rendering. It does nothing for the keystroke itself, because query is a dependency and every keystroke changes it. If typing is what stutters, keep the memo and key it on a useDeferredValue copy of the query:

tsx
import { memo, useDeferredValue, useMemo, useState } from 'react';

type Product = { id: number; name: string; price: number };

const List = memo(function List({ items }: { items: Product[] }) {
  return (
    <ul>
      {items.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
});

function ProductList({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // A keystroke first re-renders with the old deferredQuery, so this is skipped.
  const visible = useMemo(() => {
    const q = deferredQuery.toLowerCase();
    return products
      .filter((p) => p.name.toLowerCase().includes(q))
      .sort((a, b) => a.price - b.price);
  }, [products, deferredQuery]);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <List items={visible} />
    </>
  );
}

Each keystroke now renders twice. The first still sees the old deferred query, so the memo skips the filter and the input updates at once. The second recalculates the list in the background, and React abandons it if another keystroke arrives. List is wrapped in memo so the first render can skip it too.

The second reason, which matters more

The other use for useMemo has nothing to do with expensive calculations. It is about referential stability: keeping an object or array the same reference across renders so that whatever depends on it does not see a change.

tsx
// Without useMemo this object is new on every render, so the effect
// below reconnects constantly even when roomId never changed.
const config = useMemo(
  () => ({ serverUrl, roomId }),
  [serverUrl, roomId]
);

useEffect(() => {
  const connection = createConnection(config);
  connection.connect();
  return () => connection.disconnect();
}, [config]);

This is the same referential identity problem that breaks dependency arrays generally, and I walked through several versions of it in the useEffect dependency array. In that specific example, moving the object inside the effect would have been better than memoising it. Memoisation is the fallback when you cannot restructure.

useCallback caches a function

Functions declared in a component body are recreated on every render. Usually irrelevant. It matters when that function is a dependency of something else, because a new function reference reads as a change.

tsx
import { useCallback, useEffect, useState } from 'react';

function SearchBox({ onResults }: { onResults: (r: Result[]) => void }) {
  const [query, setQuery] = useState('');

  const search = useCallback(async () => {
    const results = await api.search(query);
    onResults(results);
  }, [query, onResults]);

  useEffect(() => {
    const id = setTimeout(search, 300);
    return () => clearTimeout(id);
  }, [search]); // Stable unless query or onResults changes.

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Without useCallback, search is a new function on every render, so the effect restarts its timer after every render. If the parent re-renders when results arrive, that is a loop: results, render, new timer, another request. Here is React 19.3 with a mock api.search that resolves at once and a parent that stores the results in state, left alone for two seconds with nobody typing:

bash
no useCallback   stable onResults   6 requests in 2s
useCallback      stable onResults   1 request in 2s
useCallback      inline onResults   6 requests in 2s
useEffectEvent   inline onResults   1 request in 2s

Six requests for one query, and the loop never stops on its own; a 200ms response time only slows it to four. That is a functional bug, not a performance one, and useCallback only fixes it while its own dependencies hold still. The third line is the catch: a parent that passes onResults={(r) => setResults(r)} brings the loop straight back.

useEffectEvent is the better fix in React 19.2 and later

The last line uses useEffectEvent, stable since React 19.2. It takes onResults out of the dependency array without letting it go stale, so the effect re-runs only when query changes, whatever the parent passes:

tsx
import { useEffect, useEffectEvent, useState } from 'react';

function SearchBox({ onResults }: { onResults: (r: Result[]) => void }) {
  const [query, setQuery] = useState('');
  const onDone = useEffectEvent((results: Result[]) => onResults(results));

  useEffect(() => {
    let ignore = false;
    const id = setTimeout(async () => {
      const results = await api.search(query);
      if (!ignore) onDone(results);
    }, 300);
    return () => {
      ignore = true;
      clearTimeout(id);
    };
  }, [query]); // Effect Events never go in the dependency array.

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

No useCallback anywhere, and the ignore flag stops a slow response for an old query overwriting a newer one. Keep useCallback for functions you pass to other components. A useReducer dispatch function is already stable and never needs it.

React.memo caches a component

React.memo wraps a component and skips re-rendering it when its props are shallowly equal to last time. Of the three, it is the one built to skip React work rather than JavaScript work. useMemo can do the same by caching a JSX element, but memo is the readable way to do it.

tsx
import { memo } from 'react';

type RowProps = {
  item: Item;
  onSelect: (id: string) => void;
};

// Skips re-rendering when item and onSelect are the same references.
export const Row = memo(function Row({ item, onSelect }: RowProps) {
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
});

Shallow equality is the catch. Every prop is compared with Object.is, so one inline object or arrow function in the parent defeats the whole thing:

tsx
// Row is memoised, and both of these re-render every single time anyway:
// each passes a prop that is a new reference on every parent render.
<>
  <Row item={item} onSelect={(id) => select(id)} />
  <Row item={{ ...item, selected: true }} onSelect={onSelect} />
</>

This is the most common way memoisation silently fails. React DevTools has a "Highlight updates when components render" setting that makes it visible in about ten seconds.

children defeats it too

JSX passed as children is an object, created fresh each render. A memoised component with children re-renders whenever its parent does:

tsx
import { memo, type ReactNode } from 'react';

const Panel = memo(function Panel({ children }: { children: ReactNode }) {
  return <div className="panel">{children}</div>;
});

function Parent() {
  // <Heavy /> is a new element object on every render of Parent,
  // so Panel's memo comparison always fails.
  return <Panel><Heavy /></Panel>;
}

Memoising wrapper components is rarely worth it for this reason. Memoise the leaves that are expensive, not the containers.

The custom comparison function

React.memo takes an optional second argument, a comparison that returns true when the old and new props should count as equal. It looks like an escape hatch for the object prop problem.

tsx
import { memo } from 'react';

export const Row = memo(
  function Row({ item }: { item: Item }) {
    return <li>{item.name}</li>;
  },
  // Only re-render when the fields this component reads have changed.
  (prev, next) =>
    prev.item.id === next.item.id && prev.item.name === next.item.name
);

Reach for this rarely. True means equal, the opposite of shouldComponentUpdate, and the function is a hand-maintained list of the fields the component reads: add one to the JSX later, forget the comparator, and the component shows stale data with no error. A deep equality check is no better, because you pay a recursive walk on every render to avoid a render. Passing item.id and item.name as primitive props removes the need for a comparator at all.

What will memoisation not fix?

It will not hold on to a value React decides to drop, and it will not speed up a render that is slow every time. Memoisation only skips work when the inputs are unchanged, so it cannot help the first render or any render where a dependency really changed.

useMemo is a cache, not a guarantee. React throws the cached value away when a component suspends during its first mount, and in development whenever you edit the component file, and the docs reserve the right to drop it in more cases later. So never put a side effect inside useMemo (the SaveButton bug above is exactly that), and never rely on it for identity that must hold, such as a key into a Map that outlives the render. A value that must stay stable for the lifetime of the component is a job for useRef or a lazy useState initializer, not useMemo.

A slow render is not always a re-render problem. If one render takes 200ms, memoisation only changes how often you pay it. The fix there is rendering less: virtualising a long list, splitting the work across frames, or moving the computation to the server.

The React Compiler changes the calculation

React Compiler reached 1.0 in October 2025. It runs at build time and inserts the equivalent of useMemo, useCallback and React.memo wherever it can prove that is safe, which is why all three reference pages now open by mentioning it. For new code, the 1.0 release notes recommend relying on the compiler and keeping useMemo and useCallback as escape hatches for precise control, a common one being a value used as an effect dependency.

It is not a free pass. The compiler relies on components following the rules of React, and by default it skips a component it cannot compile instead of failing the build; the compiler-powered rules in eslint-plugin-react-hooks flag the rule breaks behind those skips. In an existing codebase, leave hand-written memoisation in place or test carefully before removing it, because removing it can change the compiled output.

A better first move than memoising

Before adding any of these three, check whether the re-render can be avoided by moving state instead. State that lives higher than it needs to re-renders everything beneath it, and no amount of React.memo fixes the structure.

tsx
import { useState } from 'react';

// Every keystroke re-renders ExpensiveTree.
function PageBefore() {
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ExpensiveTree />
    </>
  );
}

// Push the state down. ExpensiveTree no longer re-renders on keystrokes.
function PageAfter() {
  return (
    <>
      <TextField />
      <ExpensiveTree />
    </>
  );
}

function TextField() {
  const [text, setText] = useState('');
  return <input value={text} onChange={(e) => setText(e.target.value)} />;
}

No memo hooks, no dependency arrays, and the problem is gone rather than managed. The same reasoning applies to Context: a provider whose value changes often re-renders every consumer, and splitting the context is a better fix than memoising downstream. I went into that trade-off in the React Context API guide, and the broader question of where state should live is the subject of React state management.

What I would actually do

If the project can run React Compiler, turn it on and stop hand-writing these in new code; in Next.js 16 that is the babel-plugin-react-compiler package plus reactCompiler: true in next.config.ts. Without it, write the component without any memoisation. Ship it. If something feels slow, open the React DevTools Profiler, record the interaction, and look at what took time. Then memoise the specific thing the profiler pointed at.

The exception is when identity is the point: an object or function that an effect or a memoised child depends on. Even then, try the structural fix first. Move the object or function inside the effect, or read the latest callback through useEffectEvent. React treats every memo as a performance hint it may drop, so if the code only works while the memo holds, the memo is hiding a bug rather than fixing one.

Everything else is a performance hypothesis, and performance hypotheses are wrong often enough that measuring is cheaper than guessing.

Frequently asked questions

Can I use useMemo instead of useCallback?

Yes. useMemo(() => fn, deps) returns the same stable function as useCallback(fn, deps), and the React docs describe useCallback as exactly that shorthand, although React's source implements the two as separate hooks. The trap is dropping the extra arrow: useMemo(fn, deps) calls fn during render and caches whatever it returns, which for an event handler is usually undefined, so the handler runs once on mount and never on click. Use useCallback for functions and useMemo for values, because the call site then says what you meant.

Does useCallback improve performance on its own?

No. A memoised callback passed to a component that is not wrapped in React.memo changes nothing, because that child re-renders whenever its parent does. In a React 19.3 test, a product grid that received a useCallback handler rendered on all 4 of its parent's renders, and once it was wrapped in React.memo it rendered only on mount. The other place useCallback earns its keep is an effect dependency, and even there, moving the function inside the effect is usually simpler, while useEffectEvent covers an effect that needs the latest callback.

When should I use useMemo and useCallback?

Use useMemo when a calculation measures over about 1ms with CPU throttling on, which is the line the React docs draw, or when a value must stay the same object because an effect or a memoised child depends on it. Use useCallback when you pass a function to a child wrapped in React.memo, or when an effect depends on the function and you cannot move it inside. For scale, in Chrome with a 4x CPU slowdown, filtering and sorting 40 products takes 0.008ms, 4,000 takes 3ms and 40,000 takes 40ms. With React Compiler enabled, write neither by default and keep them for cases that need precise control.

Why does my React.memo component still re-render?

Usually because a prop is a new reference on every render. React.memo compares props with Object.is, so an inline arrow function, an object literal or JSX children from the parent fails the comparison every time. Stabilise those props with useCallback and useMemo, or restructure so the expensive subtree does not sit under a frequently rendering parent. React.memo also cannot stop re-renders that start inside the component, when its own state or a context it reads changes, because it only compares the props its parent passes. Turn on Record why each component rendered while profiling in the React DevTools Profiler, and it reports which props, hooks or context changed.

Is React.memo the same as useMemo?

No. useMemo is a hook you call inside a component to cache a value between that component's renders. React.memo wraps a whole component and skips re-rendering it when every prop is equal to last time by Object.is. Calling useMemo inside a component does not stop that component from re-rendering, and wrapping a component in React.memo does nothing if its parent passes a new object or function each time. That is why the two appear together: useMemo and useCallback keep the props stable, and React.memo checks them.

Related Articles