DEV_NET_CORE
GET_STARTED
ReactHooks, effects, and custom hooks

Memoization with useMemo and dependency correctness

Overview

useMemo is a React Hook that caches the result of a pure calculation between renders until its dependencies change. It is mainly a performance optimization. It can help skip expensive recalculations, provide stable derived values to memoized children, or avoid recreating dependency values used by other hooks.

Code
function ProductList({
  products,
  query,
}: {
  products: Product[];
  query: string;
}) {
  const visibleProducts = useMemo(
    () => filterProducts(products, query),
    [products, query]
  );

  return <List items={visibleProducts} />;
}

Dependency correctness means the dependency array includes every reactive value used by the memoized calculation. Reactive values include props, state, and variables or functions declared inside the component. React compares dependencies with Object.is; if none changed, React can reuse the cached result.

For interviews, this topic matters because many candidates overuse useMemo, omit dependencies, depend on unstable objects, or treat memoization as a correctness tool. A strong answer explains what useMemo does, when it helps, when it is unnecessary, how dependency arrays work, and why memoized calculations must stay pure.

The practical goal is to use memoization deliberately: fix data flow first, measure or identify real cost, then memoize the smallest useful pure calculation with correct dependencies.

Core Concepts

What useMemo Does

useMemo caches a calculation result.

Code
const result = useMemo(() => calculate(input), [input]);

It takes:

  • A calculation function that takes no arguments and returns a value.
  • A dependency array containing every reactive value used inside the calculation.

On the initial render, React calls the calculation. On later renders, React compares dependencies with the previous render. If dependencies are the same by Object.is, React returns the cached value. If any dependency changed, React runs the calculation again.

useMemo Is a Performance Optimization

The component should still be correct without useMemo.

This should be correct:

Code
const visibleTodos = filterTodos(todos, tab);

Then this may improve performance:

Code
const visibleTodos = useMemo(
  () => filterTodos(todos, tab),
  [todos, tab]
);

If removing useMemo breaks behavior, the component has a design problem. State, refs, or clearer data flow may be the correct tool.

Use useMemo for:

  • Expensive pure calculations.
  • Stable props for a component wrapped in memo.
  • Stable values used as dependencies of other hooks.

Avoid using it as a blanket optimization around every value.

Dependency Arrays

The dependency array must include every reactive value used in the calculation.

Code
const visibleProducts = useMemo(
  () => products.filter((product) => product.name.includes(query)),
  [products, query]
);

products and query are dependencies because the calculation reads them.

This is wrong:

Code
const visibleProducts = useMemo(
  () => products.filter((product) => product.name.includes(query)),
  [products]
);

If query changes, the memoized value may stay stale.

The dependency list should be inline and have a constant number of items:

Code
[products, query]

Do not build dependency arrays dynamically.

What Counts as a Reactive Value

Reactive values include:

  • Props.
  • State.
  • Context values.
  • Variables declared inside the component body.
  • Functions declared inside the component body.
  • Values returned by hooks.

Example:

Code
function SearchResults({
  products,
  query,
}: {
  products: Product[];
  query: string;
}) {
  const normalizedQuery = query.trim().toLowerCase();

  const visibleProducts = useMemo(() => {
    return products.filter((product) =>
      product.name.toLowerCase().includes(normalizedQuery)
    );
  }, [products, normalizedQuery]);

  return <List items={visibleProducts} />;
}

normalizedQuery is reactive because it is declared in the component body and changes when query changes. You can also move it inside the calculation and depend on query directly.

Code
const visibleProducts = useMemo(() => {
  const normalizedQuery = query.trim().toLowerCase();

  return products.filter((product) =>
    product.name.toLowerCase().includes(normalizedQuery)
  );
}, [products, query]);

This is often clearer.

Dependency Comparison Uses Identity

React compares dependencies with Object.is.

Primitive values compare by value:

Code
const count = 1;
const query = "react";

Objects, arrays, and functions compare by reference:

Code
const options = { matchMode: "whole-word", query };

If options is created during render, it is a new object every render. Depending on it defeats memoization:

Code
const options = { matchMode: "whole-word", query };

const results = useMemo(
  () => searchProducts(products, options),
  [products, options]
);

Better:

Code
const results = useMemo(() => {
  const options = { matchMode: "whole-word", query };

  return searchProducts(products, options);
}, [products, query]);

Now the calculation depends on stable primitive inputs instead of a new object reference.

Expensive Calculations

Most calculations are not expensive enough to need useMemo.

Usually fine:

Code
const fullName = `${firstName} ${lastName}`;
const isValid = email.includes("@") && password.length >= 8;

Potentially worth memoizing:

  • Filtering or sorting thousands of items.
  • Expensive data transformation.
  • Heavy parsing.
  • Calculating layout data.
  • Creating a large derived tree.

Measure when unsure:

Code
console.time("filter products");
const visibleProducts = filterProducts(products, query);
console.timeEnd("filter products");

Profile production builds when possible. Development mode and Strict Mode can make timings misleading.

useMemo vs Derived State

Do not store derived data in state just to avoid recalculation.

Bad:

Code
const [visibleProducts, setVisibleProducts] = useState<Product[]>([]);

useEffect(() => {
  setVisibleProducts(filterProducts(products, query));
}, [products, query]);

Better:

Code
const visibleProducts = filterProducts(products, query);

If it is expensive:

Code
const visibleProducts = useMemo(
  () => filterProducts(products, query),
  [products, query]
);

The source of truth remains products and query. The memoized value is only a cached calculation result.

useMemo and memo

memo can skip re-rendering a child when its props are unchanged. useMemo can help keep a derived prop stable.

Code
const ProductTable = memo(function ProductTable({
  rows,
}: {
  rows: ProductRow[];
}) {
  return <Table rows={rows} />;
});

Parent:

Code
function ProductPage({ products, query, theme }: Props) {
  const rows = useMemo(
    () => buildRows(products, query),
    [products, query]
  );

  return <ProductTable rows={rows} />;
}

If theme changes but products and query do not, rows can keep the same reference, so ProductTable may skip re-rendering.

Without useMemo, buildRows might create a new array every render, making memo less useful.

useMemo for Hook Dependencies

Sometimes a value is used as a dependency of another hook.

Problem:

Code
const options = {
  serverUrl,
  roomId,
};

useEffect(() => {
  const connection = createConnection(options);
  connection.connect();

  return () => connection.disconnect();
}, [options]);

The object is new every render, so the effect reconnects too often.

One fix:

Code
const options = useMemo(
  () => ({ serverUrl, roomId }),
  [serverUrl, roomId]
);

useEffect(() => {
  const connection = createConnection(options);
  connection.connect();

  return () => connection.disconnect();
}, [options]);

Often better:

Code
useEffect(() => {
  const options = { serverUrl, roomId };
  const connection = createConnection(options);
  connection.connect();

  return () => connection.disconnect();
}, [serverUrl, roomId]);

Move object creation inside the effect when the object is only needed there.

useMemo vs useCallback

useMemo caches a value. useCallback caches a function reference.

These are equivalent in spirit:

Code
const handleSubmit = useMemo(() => {
  return (order: Order) => {
    submitOrder(productId, order);
  };
}, [productId]);
Code
const handleSubmit = useCallback((order: Order) => {
  submitOrder(productId, order);
}, [productId]);

Use useCallback for functions because it avoids the extra nested function shape.

Only stabilize function references when it matters:

  • Passing a callback to a memoized child.
  • Using a callback as a dependency of another hook.
  • Returning stable callbacks from a custom hook.

Pure Calculations

The useMemo calculation runs during rendering, so it must be pure.

Bad:

Code
const visibleTodos = useMemo(() => {
  todos.push({ id: "new", text: "Mutated" });
  return filterTodos(todos, tab);
}, [todos, tab]);

This mutates a prop during render.

Good:

Code
const visibleTodos = useMemo(() => {
  return filterTodos(todos, tab);
}, [todos, tab]);

In development Strict Mode, React may call the calculation more than once to help expose accidental impurities. Pure calculations are safe under repeated calls.

Cache Is Not a Semantic Guarantee

useMemo is allowed to discard its cached value for specific reasons, such as development edits or initial mount suspension. This is fine when useMemo is only a performance optimization.

Do not use useMemo to store information that must persist for correctness.

Use state when changes should trigger rendering:

Code
const [selectedId, setSelectedId] = useState<string | null>(null);

Use a ref when mutable data must persist without causing renders:

Code
const latestRequestId = useRef(0);

Use useMemo only for recalculable values.

React Compiler Nuance

Modern React tooling is moving toward more automatic memoization through the React Compiler. That reduces the need for manual useMemo in many cases when the compiler is available and enabled.

Interview-friendly answer:

  • Understand manual useMemo because many codebases still use it.
  • Do not add useMemo everywhere.
  • Keep render logic pure.
  • Prefer clear data flow.
  • Let compiler/tooling handle routine memoization where the project supports it.

The principles of dependency correctness and purity still matter.

Common Mistakes

Common mistakes include:

  • Using useMemo for every object or calculation.
  • Omitting dependencies to avoid recalculation.
  • Depending on an object or function created during render.
  • Using useMemo to make broken code work.
  • Mutating props or state inside the memoized calculation.
  • Storing derived values in state instead of calculating them.
  • Expecting useMemo to prevent all child renders without memo.
  • Measuring performance only in development mode.
  • Using useMemo where useCallback communicates intent better.
  • Depending on the cache as persistent storage.

Best Practices

Use these rules of thumb:

  • Write correct code first without memoization.
  • Memoize only expensive pure calculations or identity-sensitive values.
  • Include every reactive value used by the calculation.
  • Prefer primitive dependencies when possible.
  • Move object creation inside the memoized calculation.
  • Move object creation inside effects when the object is effect-only.
  • Use useCallback for function references.
  • Treat useMemo as a performance optimization, not storage.
  • Measure before optimizing when the cost is uncertain.

Interview Practice

Previous“You might not need an effect” patternsNext UpProper useEffect usage and cleanup