React’s useMemo is a powerful tool for performance optimization. It allows you to memoize the result of a calculation between re-renders, ensuring that expensive logic only runs when absolutely necessary.
❓ What is useMemo?
useMemo is a React Hook that caches the return value of a function. Think of it as a way to "remember" a piece of data so you don't have to recalculate it every time the component updates.
Basic Syntax
const cachedValue = useMemo(() => calculateValue(a, b), [a, b]);The Callback Function: A pure function that takes no arguments and returns the value you want to cache.
The Dependencies: A list of all reactive values (props, state, and variables declared inside your component) used inside the calculation.
🧐 Why use it?
In React, when a component's state or props change, the entire function body re-runs. If you have a complex calculation (like filtering a massive array or performing heavy math), running it on every single flicker of a UI change can lead to "jank" or lag.
Key Benefits:
Skipping expensive calculations: Only re-runs the code when inputs change.
Referential Identity: Prevents unnecessary re-renders of child components that rely on object or array props.
👀 How it Works (The Lifecycle)
Initial Render: React calls your function and stores the result.
Re-render (Dependencies unchanged): React ignores the function and returns the stored value from the previous render.
Re-render (Dependencies changed): React executes the function again, stores the new result, and returns it.
✅ Practical Example
Imagine you are building a Product Dashboard. You have a list of 5,000 products, and you need to perform two tasks:
Filter/Search through the products (Expensive).
Toggle the Theme (Dark/Light mode) of the entire page (Cheap).
The Problem
Without useMemo, every time you toggle the Theme, React re-renders the component. This forces the computer to re-filter all 5,000 products, even though the product list hasn't changed at all. This makes the theme toggle feel laggy.
<div class="infographic-usememo" id="um">
<style>
.infographic-usememo {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--radius: 8px;
--mono-font: 'IBM Plex Mono', monospace;
--body-font: 'Inter', system-ui, sans-serif;
max-width: 720px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-usememo[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-usememo * { box-sizing: border-box; min-width: 0; }
.um-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.um-code-block pre {
margin: 0; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem; overflow: auto; max-height: 420px;
}
.um-code-block code { font-family: var(--mono-font); font-size: 0.76rem; line-height: 1.72; color: #d4d4d8; white-space: pre; }
.tok-kw { color: #c678dd; }
.tok-tag { color: #e06c75; }
.tok-type { color: #61afef; }
.tok-attr { color: #d19a66; }
.tok-str { color: #98c379; }
.tok-punc { color: #6b7280; }
.tok-comment { color: #7f848e; font-style: italic; }
#umMemoBlock { display: inline; border-radius: 4px; transition: background 1.4s ease; }
#umMemoBlock.is-lit { background: rgba(34,197,94,0.3); }
.um-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center;
}
.um-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); transition: background 0.3s ease; }
.um-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.um-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.um-preview-bar span:nth-child(1) { background: #ff5f56; }
.um-preview-bar span:nth-child(2) { background: #ffbd2e; }
.um-preview-bar span:nth-child(3) { background: #27c93f; }
.um-preview-page { padding: 1.5rem 1.6rem; transition: background 0.3s ease, color 0.3s ease; }
.um-preview-page.bg-light { background: #fff; color: #1a1a1a; }
.um-preview-page.bg-dark { background: #1a1a1a; color: #f1f1f1; }
.um-theme-btn {
font-family: var(--body-font); font-size: 0.82rem; font-weight: 600; border: 1px solid currentColor;
background: transparent; color: inherit; border-radius: 100px; padding: 0.45rem 0.9rem; cursor: pointer;
margin-bottom: 0.9rem;
}
.um-search-input {
width: 100%; font-family: var(--body-font); font-size: 0.9rem; padding: 0.55rem 0.7rem; border-radius: 6px;
border: 1px solid #999; margin-bottom: 0.9rem; background: #fff; color: #000;
}
.um-preview-page.bg-dark .um-search-input { background: #2a2a2a; color: #f1f1f1; border-color: #555; }
.um-list { list-style: none; margin: 0 0 1rem; padding: 0; text-align: left; max-height: 150px; overflow-y: auto; }
.um-list li { font-size: 0.85rem; padding: 0.35rem 0; border-bottom: 1px dashed rgba(128,128,128,0.3); }
.um-list-empty { font-size: 0.85rem; opacity: 0.6; font-style: italic; }
.um-console { background: #0d0d10; border-radius: 6px; padding: 0.6rem 0.8rem; text-align: left; max-height: 110px; overflow-y: auto; }
.um-line { font-family: var(--mono-font); font-size: 0.72rem; line-height: 1.7; color: #98c379; opacity: 0; animation: um-fade 0.3s ease forwards; }
@keyframes um-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
.um-mode-row { display: flex; justify-content: center; margin-bottom: 0.9rem; }
.um-mode-btn {
font-family: var(--body-font); font-size: 0.82rem; font-weight: 600; border: none; border-radius: 100px;
padding: 0.5rem 1.1rem; cursor: pointer; transition: background 0.2s, color 0.2s, transform 0.15s;
}
.um-mode-btn:hover { transform: translateY(-1px); }
.um-mode-btn.mode-without { background: #fde3e3; color: #dc2626; }
.um-mode-btn.mode-with { background: #dcf5e3; color: #16a34a; }
@media (prefers-reduced-motion: reduce) {
.infographic-usememo *, .infographic-usememo *::before, .infographic-usememo *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="um-grid">
<div class="um-code-block">
<pre><code id="umCode"></code></pre>
</div>
<p class="um-output-label">OUTPUT</p>
<div class="um-mode-row">
<button class="um-mode-btn mode-without" id="umModeBtn" type="button">🔴 Without useMemo — click to fix</button>
</div>
<div class="um-preview">
<div class="um-preview-bar"><span></span><span></span><span></span></div>
<div class="um-preview-page bg-light" id="umPage">
<button class="um-theme-btn" id="umThemeBtn" type="button">Toggle Theme (Fast)</button>
<input class="um-search-input" type="text" id="umSearch" placeholder="Search products..." />
<ul class="um-list" id="umList"></ul>
<div class="um-console" id="umConsole"></div>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('um');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') {
if (e.data.value) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
});
var code =
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useState<span class="tok-punc">,</span> useMemo <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">ProductDashboard</span><span class="tok-punc">({</span> products <span class="tok-punc">}) {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>search<span class="tok-punc">,</span> setSearch<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-str">\'\'</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>isDarkMode<span class="tok-punc">,</span> setIsDarkMode<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-kw">false</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-comment">// 1. THE CALCULATION FUNCTION</span>\n' +
' <span class="tok-comment">// This logic is wrapped in useMemo to protect it.</span>\n' +
' <span id="umMemoBlock"><span class="tok-kw">const</span> filteredProducts <span class="tok-punc">=</span> useMemo<span class="tok-punc">(() => {</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"Filtering products... (Heavy Work)"</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-comment">// Simulating an expensive operation (e.g., complex string matching)</span>\n' +
' <span class="tok-kw">return</span> products.filter<span class="tok-punc">(</span>product <span class="tok-punc">=> {</span>\n' +
' <span class="tok-kw">return</span> product.name.toLowerCase<span class="tok-punc">().</span>includes<span class="tok-punc">(</span>search.toLowerCase<span class="tok-punc">());</span>\n' +
' <span class="tok-punc">});</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[</span>products<span class="tok-punc">,</span> search<span class="tok-punc">]);</span></span> <span class="tok-comment">// 2. THE DEPENDENCIES</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">className</span><span class="tok-punc">={</span>isDarkMode <span class="tok-punc">?</span> <span class="tok-str">\'bg-dark\'</span> <span class="tok-punc">:</span> <span class="tok-str">\'bg-light\'</span><span class="tok-punc">}></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span><span class="tok-punc">()</span> <span class="tok-punc">=></span> setIsDarkMode<span class="tok-punc">(!</span>isDarkMode<span class="tok-punc">)}></span>\n' +
' Toggle Theme (Fast)\n' +
' <span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n\n' +
' <span class="tok-punc"><</span><span class="tok-tag">input</span> \n' +
' <span class="tok-attr">type</span><span class="tok-punc">=</span><span class="tok-str">"text"</span> \n' +
' <span class="tok-attr">value</span><span class="tok-punc">={</span>search<span class="tok-punc">}</span> \n' +
' <span class="tok-attr">onChange</span><span class="tok-punc">={(</span>e<span class="tok-punc">) =></span> setSearch<span class="tok-punc">(</span>e.target.value<span class="tok-punc">)}</span> \n' +
' <span class="tok-attr">placeholder</span><span class="tok-punc">=</span><span class="tok-str">"Search products..."</span>\n' +
' <span class="tok-punc">/></span>\n\n' +
' <span class="tok-punc"><</span><span class="tok-tag">ul</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">{</span>filteredProducts.map<span class="tok-punc">(</span>p <span class="tok-punc">=></span> <span class="tok-punc"><</span><span class="tok-tag">li</span> <span class="tok-attr">key</span><span class="tok-punc">={</span>p.id<span class="tok-punc">}>{</span>p.name<span class="tok-punc">}</</span><span class="tok-tag">li</span><span class="tok-punc">>)}</span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">ul</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>';
root.querySelector('#umCode').innerHTML = code;
var memoLine =
'<span class="tok-kw">const</span> filteredProducts <span class="tok-punc">=</span> useMemo<span class="tok-punc">(() => {</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"Filtering products... (Heavy Work)"</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-comment">// Simulating an expensive operation (e.g., complex string matching)</span>\n' +
' <span class="tok-kw">return</span> products.filter<span class="tok-punc">(</span>product <span class="tok-punc">=> {</span>\n' +
' <span class="tok-kw">return</span> product.name.toLowerCase<span class="tok-punc">().</span>includes<span class="tok-punc">(</span>search.toLowerCase<span class="tok-punc">());</span>\n' +
' <span class="tok-punc">});</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[</span>products<span class="tok-punc">,</span> search<span class="tok-punc">]);</span>';
var noMemoLine =
'<span class="tok-comment">// no useMemo — this whole function re-runs on EVERY render,</span>\n' +
' <span class="tok-comment">// including renders caused by unrelated state like isDarkMode</span>\n' +
' <span class="tok-kw">const</span> filteredProducts <span class="tok-punc">=</span> <span class="tok-punc">(()</span> <span class="tok-punc">=> {</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"Filtering products... (Heavy Work)"</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">return</span> products.filter<span class="tok-punc">(</span>product <span class="tok-punc">=> {</span>\n' +
' <span class="tok-kw">return</span> product.name.toLowerCase<span class="tok-punc">().</span>includes<span class="tok-punc">(</span>search.toLowerCase<span class="tok-punc">());</span>\n' +
' <span class="tok-punc">});</span>\n' +
' <span class="tok-punc">})();</span>';
var products = [
{ id: 1, name: 'Mechanical Keyboard' },
{ id: 2, name: 'Gaming Mouse' },
{ id: 3, name: 'Wireless Headphones' },
{ id: 4, name: 'USB-C Hub' },
{ id: 5, name: 'Monitor Stand' },
{ id: 6, name: 'Webcam' }
];
var page = root.querySelector('#umPage');
var themeBtn = root.querySelector('#umThemeBtn');
var searchInput = root.querySelector('#umSearch');
var listEl = root.querySelector('#umList');
var consoleEl = root.querySelector('#umConsole');
var isDarkMode = false;
var lastSearch = null; // forces the first render to compute + log, mirroring real useMemo
var withMemo = false; // start broken, matching the "show what happens without memo" request
var modeBtn = root.querySelector('#umModeBtn');
var memoBlockEl = root.querySelector('#umMemoBlock');
function setMode(silent) {
if (withMemo) {
memoBlockEl.innerHTML = memoLine;
modeBtn.className = 'um-mode-btn mode-with';
modeBtn.textContent = '🟢 With useMemo — fixed';
} else {
memoBlockEl.innerHTML = noMemoLine;
modeBtn.className = 'um-mode-btn mode-without';
modeBtn.textContent = '🔴 Without useMemo — click to fix';
}
lastSearch = null; // reset the "memo" so the next action logs fresh, either way
if (silent) return;
// scroll the code pane to the swapped block, then flash it
memoBlockEl.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
memoBlockEl.classList.remove('is-lit');
void memoBlockEl.offsetWidth; // restart the transition
memoBlockEl.classList.add('is-lit');
setTimeout(function () { memoBlockEl.classList.remove('is-lit'); }, 1500);
}
modeBtn.addEventListener('click', function () {
withMemo = !withMemo;
setMode(false);
});
setMode(true);
function logLine(text) {
var div = document.createElement('div');
div.className = 'um-line';
div.textContent = '> ' + text;
consoleEl.appendChild(div);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function renderList(filtered) {
listEl.innerHTML = '';
if (filtered.length === 0) {
listEl.innerHTML = '<li class="um-list-empty">No products match.</li>';
return;
}
filtered.forEach(function (p) {
var li = document.createElement('li');
li.textContent = p.name;
listEl.appendChild(li);
});
}
function getFilteredProducts(search) {
// withMemo=true stands in for useMemo(..., [products, search]) — skip if search
// is unchanged. withMemo=false always recomputes, no matter what triggered the render.
if (withMemo && search === lastSearch) return null; // signal "skip, memo hit"
lastSearch = search;
logLine('Filtering products... (Heavy Work)');
return products.filter(function (product) {
return product.name.toLowerCase().indexOf(search.toLowerCase()) !== -1;
});
}
// initial render
var initial = getFilteredProducts('');
renderList(initial);
searchInput.addEventListener('input', function (e) {
var result = getFilteredProducts(e.target.value);
if (result) renderList(result); // only re-renders the list when the memo actually recomputed
});
themeBtn.addEventListener('click', function () {
isDarkMode = !isDarkMode;
page.className = 'um-preview-page ' + (isDarkMode ? 'bg-dark' : 'bg-light');
if (!withMemo) {
// no memoization: the filter function re-runs on every render, theme included
var result = getFilteredProducts(searchInput.value);
if (result) renderList(result);
}
// with memoization: isDarkMode isn't a dependency, so nothing recomputes — no call at all
});
})();
</script>Scenario A: You type "Apple" in the search box
The
searchstate updates.React checks the dependency array:
[products, search].Since
searchchanged, React executes the Calculation Function.The list is filtered, and the UI updates.
Scenario B: You click "Toggle Theme"
The
isDarkModestate updates.React re-renders the component.
React checks the dependency array:
[products, search].Neither
productsnorsearchchanged.React skips the calculation function and instantly returns the
filteredProductsfrom the previous render.The theme changes instantly without the lag of re-filtering 5,000 items.
🔴 When to Avoid useMemo
Don't wrap everything in useMemo! Overusing it can actually hurt performance because the hook itself has a small memory and computational overhead.
Cheap calculations: If you’re just adding two numbers or concatenating a string, the overhead of
useMemois higher than the calculation itself.Readability: It makes code more verbose. Only add it if you notice a performance bottleneck.
Incorrect Dependencies: If you forget a dependency, your value will be "stale" (it won't update when it should).
Pro Tip: Before reaching for useMemo, try to optimize by extracting components or passing children. Often, "state lifting" or "component composition" solves the performance issue more cleanly.
👉 useMemo vs. useCallback
These two are frequently confused. Here is the breakdown:
Feature | useMemo | useCallback |
|---|---|---|
Returns | The result of a function (value). | The function itself (memoized). |
Purpose | To avoid expensive calculations. | To avoid re-creating functions (useful for props). |
Analogy | Storing the answer to a math problem. | Storing the formula to solve the problem. |