By WebNest Studio Engineering Team
React Tutorial
useMemo and useCallback: Memoization Concepts
useMemo and useCallback are memoization hooks: they let React skip recomputing a value or recreating a function on every render, as long as its dependencies have not changed. Both exist to address performance concerns and, importantly, referential equality issues — cases where two values are logically "the same" but JavaScript treats them as different objects because they were freshly created.
Neither hook is about correctness in the way useState or useEffect are; a component will still work without them. They are optimizations, and reaching for them everywhere "just in case" often adds complexity without a measurable benefit. They matter most when an expensive calculation is being repeated unnecessarily, or when a new function/object reference is defeating another optimization such as React.memo.
Why it matters
Every render of a component re-runs its whole function, recomputing values and creating new function objects. Usually that is fine. When a calculation is expensive, or a new function reference makes a memoised child re-render needlessly, these two hooks help. Knowing when not to use them matters just as much.
useMemo: Memoizing a Computed Value
useMemo(() => computeExpensiveValue(a, b), [a, b]) only re-runs the computation when a or b changes; on any other re-render (triggered by unrelated state, for example), it reuses the previously computed result instead of recalculating it. This is valuable when a calculation is genuinely expensive — sorting a large array, filtering a big dataset — and the component re-renders often for unrelated reasons.
useCallback: Memoizing a Function Reference
useCallback(fn, [deps]) returns the same function reference across renders as long as the dependencies have not changed, instead of creating a brand-new function object every render. Since every function declared inside a component body is a new object each render, passing a fresh callback into a child on every render can defeat memoization on that child (such as a child wrapped in React.memo), because the child sees "different props" even though the callback does logically the same thing.
Referential Equality: Why This Matters
JavaScript compares objects and functions by reference, not by structure — two separately created objects with identical contents are never === equal. React's optimizations, like React.memo and the dependency arrays of useEffect/useMemo/useCallback themselves, rely on this reference comparison to decide "did this actually change?" Without memoization, a new object or function literal created during every render will always look "changed," even when its content is identical to before.
- Use useMemo when a computation is measurably expensive and its inputs rarely change compared to how often the component re-renders.
- Use useCallback when passing a callback to a memoized child component, or when the function itself is a dependency of another hook.
- Do not reach for either hook by default — the memoization bookkeeping itself has a small cost, and premature use adds noise without benefit.
Example
useMemo avoiding a repeated expensive filter, and useCallback stabilizing a handler passed to a memoized child
import { useState, useMemo, useCallback, memo } from 'react';
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>
))}
</ul>
);
});
function ProductBrowser({ products, searchTerm }) {
const [selectedId, setSelectedId] = useState(null);
const filteredProducts = useMemo(
() => products.filter((p) => p.name.toLowerCase().includes(searchTerm.toLowerCase())),
[products, searchTerm]
);
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
return (
<div>
<ExpensiveList items={filteredProducts} onSelect={handleSelect} />
{selectedId && <p>Selected product ID: {selectedId}</p>}
</div>
);
}
export default ProductBrowser;
The expensive filtering only re-runs when products or searchTerm actually change (thanks to useMemo); because handleSelect keeps the same function reference across renders (thanks to useCallback) and ExpensiveList is wrapped in React.memo, clicking a product to update selectedId does not cause ExpensiveList to needlessly re-render or log "ExpensiveList rendered" again.
Common Mistakes
- Wrapping every value and function in useMemo/useCallback "for performance" without measuring whether there is an actual performance problem to solve.
- Using useCallback without also wrapping the receiving child in React.memo — without that pairing, the stabilized function reference provides no benefit.
- Forgetting to include a value the memoized function or computation actually depends on in the dependency array, producing stale closures.
- Assuming useMemo guarantees the computation never re-runs — React may still discard the cached value and recompute it in certain situations, so it should not be relied on for correctness (e.g. for side effects).
Exercise
Do this in src/App.jsx of a Vite + React project. The component filters a list of products on every render, including when only the unrelated counter changes. Wrap the filtering in useMemo so that it runs only when the search text or the products change, and wrap the handler passed to the memoised SearchBox in useCallback.
Expected result: clicking the counter no longer re-runs the filter or re-renders SearchBox; typing in the box does.
import { memo, useState } from "react";
const PRODUCTS = ["pen", "pencil", "bag", "book", "bottle"];
const SearchBox = memo(function SearchBox({ onSearch }) {
return <input onChange={(event) => onSearch(event.target.value)} placeholder="Search" />;
});
export default function App() {
const [query, setQuery] = useState("");
const [clicks, setClicks] = useState(0);
// TODO: memoise this
const visible = PRODUCTS.filter((product) => product.includes(query));
// TODO: give this a stable identity
const handleSearch = (text) => setQuery(text);
return (
<div>
<SearchBox onSearch={handleSearch} />
<button onClick={() => setClicks(clicks + 1)}>Clicked {clicks}</button>
<ul>
{visible.map((product) => (
<li key={product}>{product}</li>
))}
</ul>
</div>
);
}
Show hints
useMemo(() => value, [dependencies])returns the cached value until a dependency changes.useCallback(fn, [])returns the same function object on every render; the setter fromuseStateis stable and need not be listed.
Show solution
import { memo, useCallback, useMemo, useState } from "react";
const PRODUCTS = ["pen", "pencil", "bag", "book", "bottle"];
const SearchBox = memo(function SearchBox({ onSearch }) {
return <input onChange={(event) => onSearch(event.target.value)} placeholder="Search" />;
});
export default function App() {
const [query, setQuery] = useState("");
const [clicks, setClicks] = useState(0);
const visible = useMemo(
() => PRODUCTS.filter((product) => product.includes(query)),
[query]
);
const handleSearch = useCallback((text) => setQuery(text), []);
return (
<div>
<SearchBox onSearch={handleSearch} />
<button onClick={() => setClicks(clicks + 1)}>Clicked {clicks}</button>
<ul>
{visible.map((product) => (
<li key={product}>{product}</li>
))}
</ul>
</div>
);
}
Quick quiz
1. What does useMemo cache?
- A. A function
- B. A component
- C. A DOM node
- D. The result of a calculation
Show answer
D. useCallback is the one that caches the function itself.
2. Why can a new function created on each render cause a child wrapped in memo to re-render?
- A. The function is a new object each time, so the prop is not equal to the previous one
- B. Functions cannot be props
- C. memo ignores functions
- D. It cannot; functions are always equal
Show answer
A. memo compares props by reference, and two separately created functions are never the same reference.
3. Should every value and function be wrapped in these hooks?
- A. Yes, always
- B. No; they add overhead and complexity, and are worth it only for costly calculations or to keep references stable
- C. Only in class components
- D. Only in production
Show answer
B. Measure first; most components are fast enough without them.
Interview questions
What is the difference between useMemo and useCallback?
Show answer
useMemo runs a function and caches the value it returns, recomputing only when a dependency changes; it is for expensive calculations and for keeping an object or array reference stable. useCallback caches the function itself, returning the same function object between renders until a dependency changes; it is for handlers passed to memoised children or used as effect dependencies. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
What is referential equality and why does it matter in React?
Show answer
Two objects, arrays or functions are equal by reference only if they are the very same object in memory; two that merely look alike are not equal. React uses this comparison for the dependencies of hooks and for the props of memoised components. An object or function created during rendering is new on every render, so it always counts as changed, which can re-run effects and re-render children unless the reference is kept stable.
Key Points to Remember
- useMemo memoizes a computed value; useCallback memoizes a function reference — both only recompute when their dependencies change.
- JavaScript compares objects and functions by reference, so a freshly created value looks "new" every render even if its contents are identical.
- useCallback is most useful when paired with a memoized child (e.g. React.memo) that would otherwise re-render due to a new function reference.
- These hooks are performance optimizations, not correctness tools — use them deliberately, not by default everywhere.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering