useMemo(calculate, deps) calls calculate during the first render, stores the result, and on every later render compares each entry of deps with the previous one using Object.is. If all of them match it returns the stored value and never calls calculate at all.
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
"react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React, { useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';
const stats = { runs: 0 };
function totalOf(rows) { // stands in for an expensive calculation
stats.runs++;
return rows.reduce((sum, r) => sum + r.price, 0);
}
function App() {
const [size, setSize] = useState(3);
const [dark, setDark] = useState(false);
const rows = useMemo(
() => Array.from({ length: size }, (_, i) => ({ id: i, price: i * 10 })), [size]);
const total = useMemo(() => totalOf(rows), [rows]);
return <div style={{ font: '15px system-ui' }}>
<button onClick={() => setSize((s) => s + 1)}>Add a row</button>{' '}
<button onClick={() => setDark((d) => !d)}>Toggle theme</button>
<ul><li>{rows.length} rows, total {total}, theme {dark ? 'dark' : 'light'}</li>
<li>totalOf ran {stats.runs} times</li></ul>
</div>;
}
createRoot(document.getElementById('root')).render(<App />);
const go = (i) => document.querySelectorAll('button')[i].click();
[100, 200].forEach((t) => setTimeout(() => go(0), t)); // two row changes
[300, 400, 500].forEach((t) => setTimeout(() => go(1), t)); // three theme changes
</script>
Five clicks give six renders, and totalOf runs three times: once at mount and once for each new row. The three theme toggles change nothing the calculation depends on, so it is skipped.
Notice that rows is memoized too. Without that, each render would build a fresh array, [rows] would never match, and the memo on total would be worthless — memoizing a value pays off only when everything it depends on is stable as well. This chain is what people get wrong by hand and what the compiler in The React Compiler gets right automatically.
The same Hook also caches JSX. An element is just an object, and React 7,897 skips a subtree whose element is reference-identical to the previous one, so useMemo(() => <Child a={a} />, [a]) gives you the effect of memo without wrapping Child.