Most explanations of useMemo and useCallback stop at “one caches a value, the other caches a function,” which is true and also not nearly enough to use them correctly. The interesting part is why that distinction matters — how React actually compares dependencies, what “referential equality” means for an object literal versus a number, and why wrapping something in either hook can make performance worse instead of better if you reach for it out of habit. Both hooks are thin wrappers around the same idea, and most of the bugs people blame on them are really just React.memo’s shallow comparison, or Object.is, doing exactly what they were always going to do.
The 8 demos below are all live and interactive, and every one that has a “before/after” split keeps both sides running next to each other so you can watch the actual difference in render counts, not just read about it. Every embed also has a Fork & Edit button that opens the exact snippet, already running, in My Code — webdevpuneet’s free in-browser editor. If you’d rather build one of these from a blank slate, the React Playground is webdevpuneet’s full in-browser React sandbox, no install or build step required for either.
The one-sentence rule
var total = useMemo(function () {
return expensiveCalc(a, b); // only re-runs when a or b actually changed
}, [a, b]);
var handleClick = useCallback(function () {
doSomething(a); // same function REFERENCE across renders
}, [a]); // as long as `a` hasn't changed
useMemo(fn, deps) calls fn during render and caches its return value; on the next render, if every entry in deps is still Object.is-equal to last time, React hands back the cached value instead of calling fn again. useCallback(fn, deps) is the exact same mechanism applied to the function itself — it’s useMemo(function () { return fn; }, deps), nothing more. Neither hook makes anything happen that wouldn’t happen anyway; both are pure “skip the work if nothing relevant changed” checks, and that check has its own small cost. The 8 demos below cover the ways that check pays off, the ways it silently does nothing, and the ways getting the dependency array wrong quietly breaks correctness rather than just performance.
1. useMemo Skips a Recalculation vs Redoing It Every Render
Click “Unrelated tick” a few times first, then click “Add 500 items.” Watch the calculation counter on each side.
The rule:
// BROKEN: recalculates on every render, tick included
var sum = expensiveSum(items);
// FIXED: only recalculates when `items` itself changes
var sum = useMemo(function () {
return expensiveSum(items);
}, [items]);
Without useMemo, the calculation is just a line of code in the render body, and React re-runs the entire component function on every render regardless of what triggered it — an unrelated state update, a parent re-rendering, anything. That means an expensive calculation pays its full cost every single time, even when its actual inputs are unchanged. useMemo intercepts that: React keeps the last computed value and the dependency array it was computed from, and on each subsequent render it compares the new dependency array to the old one entry-by-entry with Object.is. If nothing differs, the cached value is returned and the function passed to useMemo never runs at all — not even to recompute the same answer. In the demo, clicking the unrelated tick button re-renders the component, but the item list hasn’t changed, so the memoized side’s calculation counter simply doesn’t move; the unmemoized side’s counter climbs on every click regardless.
Best for: calculations that are measurably expensive (sorting or filtering large arrays, heavy math, building derived data structures) and that don’t need to change on every render. Tip: “expensive” is relative to your actual frame budget — profile before reaching for this, because the dependency-array check itself isn’t free, as demo #4 below shows directly.
2. A Plain Function Prop Breaks React.memo
Click “Unrelated tick” a few times. Neither child’s actual content changes — watch which one re-renders anyway.
The rule:
// BROKEN: a new function reference every render
var Child = React.memo(ChildImpl);
function handleClick() { doThing(); }
e(Child, { onAction: handleClick });
// FIXED: same function reference across renders
var handleClick = useCallback(function () { doThing(); }, []);
e(Child, { onAction: handleClick });
React.memo wraps a component so React can skip re-rendering it when its props are shallowly equal to last time — each prop compared individually with Object.is, the same comparison the dependency array uses. A function declared inside a component body is a brand-new object on every render, even if its code is byte-for-byte identical to the previous one; two functions are never === to each other just because they do the same thing. Pass that fresh function as a prop, and React.memo‘s shallow compare sees a prop that “changed,” so it renders the child anyway — the memoization is defeated by a single unstable prop, regardless of how many other props are perfectly stable. useCallback fixes this the same way useMemo fixes an unstable object: it hands back the identical function reference across renders where the dependency array hasn’t changed, so the shallow compare actually finds equality and React.memo gets to do its job.
Best for: any function passed as a prop to a component wrapped in React.memo — event handlers, callbacks, render props. Tip: useCallback alone accomplishes nothing if the receiving component isn’t wrapped in React.memo (or doesn’t otherwise rely on reference equality) — it stabilizes a reference, but nothing is checking that reference unless something downstream cares.
3. A Missing Dependency Serves a Stale Memoized Value
Click “+1 qty” a few times without touching price. Watch the total on each side — one of them stops updating.
The rule:
// BROKEN: reads qty, but qty isn't in the dependency array
var total = useMemo(function () {
return price * qty;
}, [price]);
// FIXED: every value the calculation reads is listed
var total = useMemo(function () {
return price * qty;
}, [price, qty]);
useMemo has no way to inspect what a function actually reads — it only knows to compare the dependency array you hand it. If the function body reads qty but qty isn’t in that array, React has no idea that value matters, and it keeps returning the cached result from the last time an array entry it does know about (price) changed. This is functionally identical to useEffect‘s missing-dependency bug — a stale closure — except instead of an effect doing the wrong thing at the wrong time, it’s a value silently drifting out of sync with the UI that’s showing it, without any error, warning, or crash. In the demo, incrementing quantity does nothing to the broken total until price is also touched, at which point the memo recalculates and briefly looks “caught up” using whatever quantity happens to be current at that moment — which is correct by coincidence, not by correctness.
Best for: catching a calculation that silently reads more state than its dependency array admits to — this is exactly what the ESLint exhaustive-deps rule checks for, and it applies to useMemo and useCallback identically to useEffect. Tip: if you find yourself wanting to leave a value out of the array on purpose “because it doesn’t need to trigger a recompute,” that’s usually a sign the value belongs in a ref instead, not that the array should lie about it.
4. Memoizing a Cheap Calculation Buys You Nothing
Type in the box. The dependency changes on every keystroke, so both sides recalculate the same number of times.
The rule:
// No useMemo -- just call it
var shouted = shout(text);
// useMemo'd -- but `text` changes on every keystroke,
// so the dependency check never has anything to skip
var shouted = useMemo(function () { return shout(text); }, [text]);
useMemo only saves work when the dependency array is actually stable across some renders — when its comparison genuinely finds a match some of the time. If the calculation’s only dependency changes on essentially every render (a text input bound to every keystroke is the clearest case), the comparison never succeeds, the cached branch never gets taken, and the calculation runs exactly as often as it would have without useMemo at all. Worse, it’s not actually free: React still has to allocate space to store the previous dependency array and result, compare each array entry with Object.is on every render, and hold onto that memory for the component’s lifetime. For a calculation as cheap as an uppercase transform, that bookkeeping is very likely more expensive than the calculation it’s guarding — the memoization overhead exceeds the cost it was meant to avoid, which is the textbook shape of a premature optimization.
Best for: nothing, deliberately — this demo exists to show the failure mode of wrapping everything in useMemo reflexively. Tip: ask two questions before adding it: is the calculation itself actually expensive, and does its dependency array stay the same across a meaningful number of renders? If either answer is no, skip it.
5. useCallback Stabilizes a Function Inside useEffect’s Deps
Click Start on each side and watch the effect-run count. One climbs on its own; the other runs once and stops.
The rule:
// BROKEN: fetchData is new every render, so this dep is always "different"
function fetchData() { return getSomething(); }
useEffect(function () { fetchData(); }, [fetchData]);
// FIXED: same reference, so the effect only re-runs when it should
var fetchData = useCallback(function () { return getSomething(); }, []);
useEffect(function () { fetchData(); }, [fetchData]);
This is the useEffect classic — an infinite update loop — arriving from the function side instead of the object side. A function declared fresh inside a component is a new reference every render, exactly like an inline object literal; putting it directly into an effect’s dependency array means the effect sees a “changed” dependency on every render it causes, and if that effect also updates state (directly, or via whatever fetchData does), the loop is complete: render creates a new function, effect sees it as different and re-runs, re-running updates state, updating state triggers a render, which creates another new function. useCallback breaks the cycle the same way useMemo breaks it for an object dependency — by returning the identical function reference across renders where its own dependencies haven’t changed, so the effect’s comparison finally finds a match and stops re-running. This demo caps the broken side at 40 runs so Forking it and removing useCallback can’t freeze your tab.
Best for: any function defined inside a component that also appears in another hook’s dependency array — useEffect, another useMemo, or a custom hook. Tip: this is the functional twin of demo #6 below — same underlying cause, same fix shape, different hook holding the reference.
6. An Unmemoized Object Wrecks a Downstream useMemo
Click “Unrelated tick” a few times, then try “Raise min price.” Watch the filter-run count on each side.
The rule:
// Parent hands the child a new object every render
var options = { min: minPrice };
// Child does its own memoized work, keyed on that object
var filtered = useMemo(function () {
return items.filter(function (n) { return n >= options.min; });
}, [options]); // never stable unless the PARENT memoized `options`
Memoization composes across component boundaries, which means an unmemoized value at the top can defeat a correctly-written useMemo several components downstream. The child in this demo does everything right — it memoizes its own filtering work, keyed on the options object it receives as a prop. But if the parent builds that object as a fresh literal on every render, the child’s dependency array is comparing a new object to a new object every time, and the comparison can never succeed, no matter how carefully the child itself is written. The fix has to happen at the source: the parent memoizing options with its own useMemo, keyed on the actual primitive value (minPrice) that determines its contents, is what gives the child something stable to compare against. This is also exactly why passing raw object or array literals as props to a memoized child is worth a second look, even when the child looks correctly optimized on its own.
Best for: any object, array, or function that’s built in a parent and handed to a child that memoizes work based on it — the stability has to be established where the value is created, not where it’s consumed. Tip: if you’re debugging a memo that “should” be skipping work but isn’t, check one level up the component tree before assuming the memo itself is wrong.
7. useMemo Caches a Value, useCallback Only Caches a Function
Click “Unrelated tick” a few times. Both sides display the same greeting — watch the compute counter on each.
The rule:
// useMemo -- caches the RESULT of calling the function
var greeting = useMemo(function () { return buildGreeting(name); }, [name]);
// useCallback -- caches the FUNCTION, not its result
var getGreeting = useCallback(function () { return buildGreeting(name); }, [name]);
var greeting = getGreeting(); // still recomputes on every render this runs
Because useCallback(fn, deps) is really just useMemo(function () { return fn; }, deps), what it caches is the function object itself, not whatever that function would return if you called it. Calling getGreeting() executes the function body immediately, every time you call it — the stable reference has nothing to do with how many times the body has run, only with whether the reference handed to whoever’s holding it changed. useMemo, by contrast, calls the function itself internally and caches the return value, so the caller never re-executes anything on a render where the dependency array matched. Reaching for useCallback to “cache a value” is a common mix-up specifically because both hooks look nearly identical at the call site — the difference only becomes visible once you notice one of them requires an extra pair of parentheses to actually use, and that those parentheses re-run the whole calculation.
Best for: making the useMemo/useCallback distinction concrete rather than definitional — reach for useMemo when you want a cached value, useCallback when you want a cached function reference to hand to something else (an event handler, a dependency array, a memoized child). Tip: if you’re about to call the thing useCallback gave you and use its return value directly, you almost certainly wanted useMemo.
8. useMemo Only Remembers Last Render, Not History
Click 3, then 7, then 3 again. Watch the recompute count — one side recomputes for the repeat, the other doesn’t.
The rule:
// useMemo only compares against the PREVIOUS render's deps -- one slot
var result = useMemo(function () { return slowSquare(n); }, [n]);
// A real cache remembers every key it has ever seen
if (!(n in cacheRef.current)) cacheRef.current[n] = slowSquare(n);
var result = cacheRef.current[n];
useMemo is often described loosely as “caching,” which invites the assumption that once a value has been computed for a given input, it stays cached for that input forever. It doesn’t — React holds exactly one cached result at a time, tied to exactly one previous dependency array, and the comparison is only ever against the immediately preceding render. Set n to 3, then 7, then back to 3: the third render’s dependency array ([3]) is compared only against the second render’s ([7]), which differs, so the function runs again — even though 3 was computed just one step earlier. A genuine cache, keyed by every value it has ever seen (the ref-based object on the right), remembers that earlier result and returns it without recomputation. This is the clearest possible proof that useMemo is a last-render optimization, not a memoization table — the word “memo” describes the mechanism, not a promise about how long anything is remembered.
Best for: correcting the mental model before it causes a real bug — code that assumes a memoized value is safe to treat as permanently cached (for cache invalidation, deduplication, or “compute once” logic) will be wrong the moment inputs oscillate. Tip: if you actually need a value cached across many distinct inputs over a component’s lifetime, that’s a ref holding a real lookup structure (as in the fixed side here), not useMemo.
Common pitfalls
- Assuming either hook prevents a calculation from running instead of just sometimes skipping it. Demos #1 and #4 are the same mechanism with opposite outcomes — it only pays off when the dependency array is actually stable across renders.
- Wrapping a callback in useCallback without wrapping the component that receives it in React.memo. Demo #2’s fix does nothing if nothing downstream is checking reference equality in the first place.
- Leaving a value out of the dependency array because “it doesn’t need to trigger a recompute.” Demo #3 shows the actual result — not a missed update, but a wrong one that looks correct by coincidence.
- Memoizing a value one level too late in the component tree. Demo #6 shows a correctly-written child memo defeated entirely by an unmemoized object one level up.
- Treating useMemo’s cached value as a permanent, key-based cache. Demo #8 is the direct proof — it only ever remembers the immediately previous render, nothing further back.
Frequently asked questions
Should I wrap every function I pass as a prop in useCallback?
No — only when the function is passed to something that actually checks reference equality, most commonly a child wrapped in React.memo or another hook’s dependency array (as in demos #2 and #5). If the receiving component isn’t memoized and nothing downstream compares the reference, useCallback adds its own small overhead for zero benefit, the same overengineering demo #4 shows for useMemo.
Does useMemo guarantee the calculation won’t run twice?
No. The React docs are explicit that useMemo is a performance optimization, not a semantic guarantee — React may discard a cached value and recalculate in some circumstances (for example, to free memory for offscreen components), so code should never depend on useMemo for correctness the way you’d depend on a ref or actual state. Every demo above that relies on stability for correctness (like #2, #5, and #6) is really relying on the hook’s typical behavior, and would still need to be correct even if a recompute happened more often than expected.
What does React.memo’s “shallow comparison” actually mean?
React.memo compares each prop individually to its previous value using Object.is — the same check used for dependency arrays. Primitives (numbers, strings, booleans) compare by value, so 5 === 5 is true regardless of how the 5 was produced. Objects, arrays, and functions compare by reference, so two objects with identical contents are never equal unless they’re the literal same object in memory — which is exactly the gap demo #2 and demo #6 both exploit and then close.
Is useMemo the same thing as React Compiler’s automatic memoization?
Conceptually related, mechanically different. React Compiler (stable since its 1.0 release in late 2025, and opt-in per project) analyzes your component code at build time and inserts memoization automatically, aiming to make manual useMemo/useCallback calls unnecessary in the components it covers. Every demo in this post uses the hooks manually and explicitly, which is still the only option outside a React Compiler-enabled build, and understanding what the hooks actually do is exactly what makes it possible to tell whether an automated tool got it right.
Why does the demo cap the broken effect at 40 runs instead of letting it run free?
Demo #5’s broken side is a genuine infinite loop — without a cap, it would re-render as fast as the browser can manage, which would pin a tab at high CPU and make the rest of the page unresponsive. The cap exists purely so you can Fork the demo, intentionally remove the fix, and see the failure mode safely; the underlying bug is exactly as unbounded in real code as it looks here.
The fastest way to internalize the difference between these two hooks is to Fork & Edit demo #7 — the side-by-side one — and add a third column that wraps the calculation in neither hook at all. Watch its compute counter behave identically to the useCallback column, not the useMemo one, and the distinction stops being something you have to remember. Once you’ve got a version you like, save it to My Code and it’s yours to keep, tweak further, or drop into a real project.
Recommended next step: open the React Playground — webdevpuneet’s full in-browser React sandbox — and rebuild demo #6’s parent-to-child memoization chain from scratch with your own data. Typing it out yourself, not just reading it, is what makes the pattern stick. For more tutorials like this one, browse the React category on the blog.
Discover more from WebDevPuneet
Subscribe to get the latest posts sent to your email.




