The useCallback hook is a staple of React performance optimization. At its core, it’s about memoization—specifically, ensuring that a function instance remains the same between renders unless its dependencies change.
❓What is useCallback?
In React, every time a component re-renders, all functions defined inside it are "re-created." While JavaScript handles this quickly, it causes issues when those functions are passed as props to optimized child components.
useCallback returns a memoized version of the callback function that only changes if one of the dependencies has changed.
The Syntax
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b], // Dependency array
);🧐 Why do we need it?
To understand useCallback, you have to understand Referential Equality.
In JavaScript:
{} === {}is false.() => {} === () => {}is false.
Even if two functions do the exact same thing, they occupy different spots in memory. When you pass a function to a child component, the child sees a "new" prop every time the parent renders, which triggers a re-render of the child—even if the child is wrapped in React.memo.
🧠 The Core Use Cases
A. Preventing Unnecessary Child Re-renders
This is the most common use case. When a child component is wrapped in React.memo, it only re-renders if its props change. Without useCallback, the function prop "changes" every time.
B. Functions as Dependencies
If you have a function that is used inside a useEffect hook, React will warn you to include that function in the dependency array. If you don't wrap the function in useCallback, the useEffect will run on every single render.
🔴 Common Pitfalls & Best Practices
Don't Overuse It: Memoization has an overhead cost. If you're not passing the function to a memoized child or using it in a dependency array,
useCallbackactually makes your code slightly slower and more complex.Dependency Array Honesty: Always include every reactive value (state, props) used inside the function. If you skip one, your function will use "stale" variables from a previous render.
Pairs with
React.memo: UsinguseCallbackon a prop is useless unless the child component is actually optimized withReact.memo.
🚀 Practical Example
Here is how it looks in a real-world scenario where a button component shouldn't re-render needlessly:
<div class="infographic-usecallback" id="uc">
<style>
.infographic-usecallback {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--good: #16a34a;
--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-usecallback[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-usecallback * { box-sizing: border-box; min-width: 0; }
.uc-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.uc-code-block pre {
margin: 0; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem; overflow: auto; max-height: 400px;
}
.uc-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; }
.uc-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;
}
.uc-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); }
.uc-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.uc-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.uc-preview-bar span:nth-child(1) { background: #ff5f56; }
.uc-preview-bar span:nth-child(2) { background: #ffbd2e; }
.uc-preview-bar span:nth-child(3) { background: #27c93f; }
.uc-preview-page { background: #fff; padding: 1.5rem 1.6rem; text-align: center; color: #000; }
.uc-preview-page p { margin: 0 0 0.9rem; font-size: 0.95rem; }
.uc-real-btn {
font-family: var(--body-font); font-size: 0.85rem; font-weight: 600; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.55rem 1rem; cursor: pointer; margin: 0 0.3rem 0.9rem;
transition: transform 0.15s;
}
.uc-real-btn:hover { transform: translateY(-1px); }
.uc-real-btn.uc-ship { background: #1a1a1a; }
.uc-badge {
display: inline-block; font-family: var(--mono-font); font-size: 0.68rem; font-weight: 700; color: var(--good);
background: #dcf5e3; border-radius: 100px; padding: 0.15rem 0.6rem; margin-bottom: 0.9rem;
}
.uc-console { background: #0d0d10; border-radius: 6px; padding: 0.6rem 0.8rem; text-align: left; max-height: 130px; overflow-y: auto; }
.uc-line { font-family: var(--mono-font); font-size: 0.74rem; line-height: 1.7; color: #98c379; opacity: 0; animation: uc-fade 0.3s ease forwards; }
@keyframes uc-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
.infographic-usecallback *, .infographic-usecallback *::before, .infographic-usecallback *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="uc-grid">
<div class="uc-code-block">
<pre><code id="ucCode"></code></pre>
</div>
<p class="uc-output-label">OUTPUT</p>
<div class="uc-preview">
<div class="uc-preview-bar"><span></span><span></span><span></span></div>
<div class="uc-preview-page">
<p>Items: <span id="ucCount">0</span></p>
<button class="uc-real-btn" id="ucAddBtn" type="button">Add to Cart</button>
<button class="uc-real-btn uc-ship" id="ucShipBtn" type="button">Update Shipping</button>
<div><span class="uc-badge" id="ucRenderBadge">ShippingButton renders: 1</span></div>
<div class="uc-console" id="ucConsole"></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('uc');
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> useCallback <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">const</span> <span class="tok-type">ShippingButton</span> <span class="tok-punc">=</span> <span class="tok-type">React</span><span class="tok-punc">.</span>memo<span class="tok-punc">((</span><span class="tok-punc">{</span> onClick <span class="tok-punc">}) => {</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"Button rendered"</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>onClick<span class="tok-punc">}></span>Update Shipping<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">>;</span>\n' +
'<span class="tok-punc">});</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">ProductPage</span><span class="tok-punc">({</span> productId <span class="tok-punc">}) {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>count<span class="tok-punc">,</span> setCount<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-expr">0</span><span class="tok-punc">);</span>\n\n' +
' <span class="tok-comment">// Without useCallback, ShippingButton re-renders every time \'count\' changes</span>\n' +
' <span class="tok-kw">const</span> handleShipping <span class="tok-punc">=</span> useCallback<span class="tok-punc">(() => {</span>\n' +
' console.log<span class="tok-punc">(</span><span class="tok-str">"Processing product:"</span><span class="tok-punc">,</span> productId<span class="tok-punc">);</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[</span>productId<span class="tok-punc">]);</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>Items<span class="tok-punc">:</span> <span class="tok-punc">{</span>count<span class="tok-punc">}</</span><span class="tok-tag">p</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> setCount<span class="tok-punc">(</span>count <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">)}></span>Add to Cart<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-type">ShippingButton</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>handleShipping<span class="tok-punc">}</span> <span class="tok-punc">/></span>\n' +
' <span class="tok-punc"></></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>';
root.querySelector('#ucCode').innerHTML = code;
var countEl = root.querySelector('#ucCount');
var addBtn = root.querySelector('#ucAddBtn');
var shipBtn = root.querySelector('#ucShipBtn');
var renderBadge = root.querySelector('#ucRenderBadge');
var consoleEl = root.querySelector('#ucConsole');
var count = 0;
var shipRenders = 1; // ShippingButton already rendered once on mount
function logLine(text) {
var div = document.createElement('div');
div.className = 'uc-line';
div.textContent = '> ' + text;
consoleEl.appendChild(div);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
logLine('Button rendered'); // ShippingButton's first (and, thanks to memo + useCallback, only) render
addBtn.addEventListener('click', function () {
count += 1;
countEl.textContent = count;
// ShippingButton does NOT re-render: React.memo sees the same onClick reference,
// because useCallback's dependency (productId) never changed.
});
shipBtn.addEventListener('click', function () {
logLine('Processing product: 42');
});
})();
</script>1. The "Problem" Component: ShippingButton
const ShippingButton = React.memo(({ onClick }) => {
console.log("Button rendered");
return <button onClick={onClick}>Update Shipping</button>;
});React.memo: This is a "Higher Order Component." It tells React: "Only re-render this button if its props (likeonClick) actually change."The Console Log: This is our diagnostic tool. If you see "Button rendered" in your console when you haven't touched the shipping settings, it means your optimization failed.
2. The State Trigger: count
const [count, setCount] = useState(0);
// ...
<button onClick={() => setCount(count + 1)}>Add to Cart</button>When you click "Add to Cart," the
countstate updates.Crucial Concept: When state updates, the entire
ProductPagefunction runs again from top to bottom.
3. The Solution: useCallback
Without useCallback, the handleShipping function would be "new" (a different spot in memory) on every single click of the "Add to Cart" button. This would trick React.memo into thinking the props changed, causing the ShippingButton to re-render needlessly.
const handleShipping = useCallback(() => {
console.log("Processing product:", productId);
}, [productId]);The Memoization: React stores this function definition in a special cache.
The Dependency Array
[productId]:If
productIdstays the same (e.g., "iPhone-15"), React keeps returning the exact same function instance from the cache.Because the instance is identical,
React.memolooks at theonClickprop, sees it hasn't changed, and skips re-rendering the button.If you switch to a different product page (changing
productId), React throws away the old function and creates a new one to ensure the logic stays fresh.
Summary of the "Flow"
User clicks "Add to Cart": State (
count) changes.ProductPagere-renders: React encountersuseCallback.Stability Check: React sees
productIdhasn't changed $\rightarrow$ it returns the oldhandleShippingreference.Prop Comparison:
ShippingButton(wrapped inmemo) compares its newonClickprop to its old one.The Result: Since they are the same instance, the button does not re-render.
This saves CPU cycles and prevents the UI from flickering or lagging in complex applications.