Senior 8 min · March 05, 2026
React useMemo and useCallback

Stale useCallback — User A IDs Sent After User B Logs In

After user B logs in, useCallback still sends user A's ID in API payload because userId was omitted from deps.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Production
production tested
June 10, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • useMemo caches computed values; useCallback caches function references.
  • Both preserve referential identity so React.memo can skip re-renders.
  • useCallback(fn, deps) is identical to useMemo(() => fn, deps) internally.
  • Profiling is essential: memoize only when the profiler shows a real bottleneck.
  • The most common production mistake: applying useCallback without React.memo on the child.
  • The hardest bug: stale closures from missing dependencies – always enable exhaustive-deps.
✦ Definition~90s read
What is React useMemo and useCallback?

useMemo and useCallback are React hooks that control when values and functions are recreated between renders. They don't make your code faster by default — in fact, overusing them often slows things down. The real problem they solve is referential identity: ensuring that a reference (an object, array, or function) stays the same across renders unless its dependencies change.

Imagine you run a bakery and someone asks for a cake.

This matters because React's memoization features (React.memo, useMemo, useCallback) and many third-party libraries (like Redux's useSelector or React Query's useQuery) rely on stable references to avoid unnecessary re-renders or recomputations. Without them, a function defined inside a component body gets a new identity every render, causing child components to re-render even when nothing meaningful changed — which is exactly the bug described in this article's title, where stale user IDs leak across sessions after a login switch.

Plain-English First

Imagine you run a bakery and someone asks for a cake. You could bake a fresh one every single time — even if the recipe and ingredients haven't changed at all. That's wasteful. Instead, you write down the finished cake on a notepad and if the same order comes in with the same ingredients, you just hand over the notepad copy. useMemo does exactly that for computed values. useCallback does the same thing but for the recipe itself — it hands you back the same recipe function so you don't accidentally confuse your staff into thinking it's a brand-new order every time.

Every React developer hits a wall eventually. The app works, the logic is sound, but child components are re-rendering for no apparent reason, expensive calculations are running on every keystroke, and the profiler is showing a wall of orange. The usual suspects? Inline functions and unguarded expensive computations passing straight through to memoized children. useMemo and useCallback are React's built-in escape hatches from this exact problem — but they're also among the most misused hooks in the entire ecosystem.

The core problem both hooks solve is referential instability. In JavaScript, a new function literal or a newly computed object created inside a render cycle gets a brand-new memory address every single time, even if its contents are identical. When you pass that value down as a prop, React.memo and PureComponent see a different reference and trigger a re-render, even though nothing meaningful changed. useMemo caches a computed value between renders. useCallback caches a function definition between renders. Both return the same reference until their dependency array changes.

After reading this article you'll be able to: correctly identify when memoization actually helps versus when it adds overhead with zero gain, confidently explain the difference between useMemo and useCallback at a referential identity level, avoid the three most common production mistakes that turn these hooks into performance liabilities, and write genuinely optimized React component trees that hold up under profiler scrutiny.

Why useMemo and useCallback Are Not Optimization Shortcuts

useMemo and useCallback are React hooks that memoize values and functions respectively, preventing unnecessary re-computation or re-creation across renders. useMemo caches the result of a pure function until its dependencies change; useCallback caches a function reference itself. Both rely on referential equality — if dependencies haven't changed, the previous reference is returned. This is O(1) lookup, not O(n) diffing. In practice, they only help when the child component uses React.memo or when a hook like useEffect lists the value/function as a dependency. Without that, memoization is overhead with zero benefit. Use them when you have measured a performance bottleneck — never preemptively. The real value is preventing cascading re-renders in deep component trees or stabilizing dependencies for effects that fetch data or subscribe to streams.

Default assumption is wrong
useMemo and useCallback do not prevent re-renders of the component they're in — they only prevent re-renders of children wrapped in React.memo.
Production Insight
A team wrapped every callback in useCallback across a 500-component tree, causing a 30% render slowdown from the overhead of dependency array comparisons.
Symptom: React DevTools showed every component re-rendering, but memoized callbacks were never consumed by memoized children.
Rule: Only memoize when the child is memoized or the callback is a dependency — otherwise you're paying for nothing.
Key Takeaway
useMemo/useCallback are for referential stability, not computation speed.
Without React.memo on the child, memoizing a callback is dead code.
Profile first, memoize second — premature memoization is an anti-pattern.
Stale useCallback: User A IDs Sent After User B Logs In THECODEFORGE.IO Stale useCallback: User A IDs Sent After User B Logs In Flow from referential identity to memoization failure and stale closures Referential Identity Function/object references change on every render useCallback Stabilizes Returns same reference unless deps change Stale Closure Captured deps from previous render Dependency Array Mismatch Missing or stale deps cause outdated values Memoization Failure Child re-renders with stale props User A IDs Sent After User B logs in, wrong data flows ⚠ Missing deps in useCallback cause stale closures Always include all referenced variables in the dependency array THECODEFORGE.IO
thecodeforge.io
Stale useCallback: User A IDs Sent After User B Logs In
React Usememo Usecallback

Referential Identity — The Root Problem Both Hooks Actually Solve

Before touching either hook, you need a firm grip on why JavaScript's equality model bites React so hard. Primitive values like numbers and strings compare by value. But functions and objects compare by reference — their memory address. Two functions with identical bodies are not equal in JavaScript.

Every time a React component renders, its function body executes top to bottom. Any function or object defined inside that body is born fresh — a new allocation, a new address. That's by design in JavaScript, but it's a silent killer in React's prop-passing model.

React.memo wraps a child component and bails out of re-rendering if its props are shallowly equal to the previous render. Shallow equality means it checks object references, not deep values. So if your parent passes an inline callback or a freshly computed array, React.memo sees a new reference on every parent render and the bail-out never fires. useMemo and useCallback exist specifically to preserve referential identity across renders so that memoized children can actually do their job.

ReferentialIdentityDemo.jsxJAVASCRIPT
1
2
3
4
5
6
7
import React, { useState } from 'react';

// This child is wrapped in React.memo — it SHOULD skip re-renders
// if its props haven't changed. But watch what actually happens.
const ProductTag = React.memo(({ label, onRemove }) => {\n  console.log(`ProductTag rendered: \"${label}\"`);\n  return (\n    <span>\n      {label}\n      <button onClick={onRemove}>✕</button>\n    </span>\n  );\n});\n\nexport default function ProductList() {\n  const [cartCount, setCartCount] = useState(0);\n  const [tags] = useState(['React', 'TypeScript', 'Node']);\n\n  // ❌ Problem: this function is recreated on EVERY render of ProductList.\n  // Even though its logic never changes, it gets a fresh memory address\n  // each time cartCount changes. React.memo on ProductTag is useless here.\n  const handleRemove = (tag) => {\n    console.log(`Removing tag: ${tag}`);\n  };\n\n  return (\n    <div>\n      {/* Clicking \"Add to Cart\" triggers a re-render of ProductList.\n          That re-render creates a new handleRemove function.\n          React.memo sees a new prop reference → re-renders ALL ProductTags.\n          Check the console — all three tags log on every cart click. */}\n      <button onClick={() => setCartCount(c => c + 1)}>\n        Add to Cart ({cartCount})\n      </button>\n\n      {tags.map(tag => (\n        <ProductTag\n          key={tag}\n          label={tag}\n          onRemove={() => handleRemove(tag)} // new function on every render\n        />\n      ))}\n    </div>\n  );\n}",
        "output": "// On first render:\nProductTag rendered: \"React\"\nProductTag rendered: \"TypeScript\"\nProductTag rendered: \"Node\"\n\n// After clicking 'Add to Cart':\nProductTag rendered: \"React\"\nProductTag rendered: \"TypeScript\"\nProductTag rendered: \"Node\"\n// ↑ All three re-render even though tags didn't change.\n// React.memo was completely bypassed by the unstable function reference."
      }

useCallback — Stabilising Function References Across Renders

useCallback(fn, deps) returns a memoized version of the function fn. React guarantees it returns the exact same function reference between renders as long as none of the values in the deps array have changed. The moment a dependency changes, useCallback throws away the cached version and stores the new function.

The mental model: useCallback doesn't make your function faster. It makes your function stable. That stability is what lets React.memo on child components actually bail out. Without it, you're memoizing a child but feeding it a moving target.

A critical internal detail: useCallback(fn, deps) is literally identical to useMemo(() => fn, deps). React's source code even shows this — useCallback is just a convenience wrapper. Understanding this equivalence helps you reason about both hooks from first principles rather than memorising two separate mental models.

The dependency array follows the same rules as useEffect. Every value from the component scope that's used inside the callback — props, state, context values, other variables — must be listed. ESLint's exhaustive-deps rule from eslint-plugin-react-hooks is your best friend here; enable it and treat its warnings as errors.

StableCallbackDemo.jsxJAVASCRIPT
1
2
3
4
5
import React, { useState, useCallback } from 'react';

const ProductTag = React.memo(({ label, onRemove }) => {\n  // This log will now only fire on initial mount, NOT on cart updates.\n  console.log(`ProductTag rendered: \"${label}\"`);\n  return (\n    <span style={{ marginRight: 8 }}>\n      {label}\n      <button onClick={onRemove}>✕</button>\n    </span>\n  );\n});\n\nexport default function ProductList() {\n  const [cartCount, setCartCount] = useState(0);\n  const [tags, setTags] = useState(['React', 'TypeScript', 'Node']);\n\n  // ✅ useCallback returns the SAME function reference on every render\n  // as long as `tags` (the dependency) hasn't changed.\n  // Now React.memo on ProductTag can actually do its job.\n  const handleRemove = useCallback(\n    (tagToRemove) => {\n      // We reference `setTags` here. The setter from useState is guaranteed\n      // stable by React, so it doesn't need to be in deps.\n      // We use the functional update form to avoid needing `tags` in deps.\n      setTags(currentTags => currentTags.filter(t => t !== tagToRemove));\n    },\n    [] // Empty array: this function never needs to be recreated.\n  );\n\n  return (\n    <div>\n      <button onClick={() => setCartCount(c => c + 1)}>\n        Add to Cart ({cartCount})\n      </button>\n\n      {tags.map(tag => (\n        <ProductTag\n          key={tag}\n          label={tag}\n          // ✅ We still create a new arrow here for the specific tag,\n          // but see the tip below for how to handle this pattern cleanly.\n          onRemove={() => handleRemove(tag)}\n        />\n      ))}\n    </div>\n  );\n}\n\n// ─── Cleaner pattern: pass the raw handler + tag ID as separate props ──────\n// This way React.memo compares (handleRemove === handleRemove) ✅\n// and (tag === tag) ✅ — both stable, no unnecessary renders.\nconst CleanProductTag = React.memo(({ label, tagId, onRemove }) => {\n  console.log(`CleanProductTag rendered: \"${label}\"`);\n  // Bind the tag ID inside the child — the parent stays clean.\n  return (\n    <span>\n      {label}\n      <button onClick={() => onRemove(tagId)}>✕</button>\n    </span>\n  );\n});",
        "output": "// On first render:\nProductTag rendered: \"React\"\nProductTag rendered: \"TypeScript\"\nProductTag rendered: \"Node\"\n\n// After clicking 'Add to Cart' (cartCount changes, tags don't):\n// → Nothing logged. All three ProductTags bailed out. ✅\n\n// After removing 'TypeScript' (tags array changes):\n// → handleRemove is recreated (deps changed)\n// → React.memo re-evaluates remaining tags\nProductTag rendered: \"React\"\nProductTag rendered: \"Node\""
      }

useMemo — Caching Expensive Computed Values Across Renders

useMemo(() => computeValue(), deps) runs the factory function once, stores the result, and returns that same result on every subsequent render until a dependency changes. It's the hook for when the computation is the bottleneck, not the reference.

The two valid use cases for useMemo are: (1) genuinely expensive computations — think filtering or sorting thousands of records, running a heavy algorithmic transformation, or deriving complex state trees; and (2) referentially stable objects and arrays that need to be passed to memoized children or used in other hooks' dependency arrays.

That second use case is subtler but equally important. If you compute an options object inside a component and pass it to a child wrapped in React.memo, or use that object in a useEffect dependency array, every render produces a new reference, triggering unnecessary effects or child renders. useMemo pins that object to a stable reference.

One internal detail worth knowing: React can and does discard the memoized value in some situations — specifically in development mode during Strict Mode double-invocation, and in future concurrent features where React may interrupt and restart renders. Never rely on useMemo as the single source of truth for side effects or critical logic. It's a performance hint to React, not a guarantee.

ExpensiveFilterDemo.jsxJAVASCRIPT
1
2
3
4
5
6
7
import React, { useState, useMemo } from 'react';

// Simulate a large product catalogue — 50,000 items.
function generateCatalogue(size) {
  return Array.from({ length: size }, (_, index) => ({
    id: index,
    name: `Product ${index}`
Output
// On initial render:
filter: 2.41ms ← runs once to populate the initial list
Showing 50000 results
// After typing 'Product 1' in search:
filter: 1.87ms ← reruns because searchTerm changed ✅
Showing 11112 results for "Product 1"
// After clicking Wishlist (+1):
// → No 'filter:' log at all ✅
// → useMemo returned the cached value, zero filter cost
Showing 11112 results for "Product 1" ← same result, instant
Pro Tip: Benchmark Before You Memoize
Wrap your computation in console.time/console.timeEnd before adding useMemo. If it consistently takes less than 1ms on mid-range hardware, skip useMemo — you'll add more overhead with the hook bookkeeping than you save. React's own team says memoize only when profiler evidence demands it.
Production Insight
Filtering 50k items on every keystroke without useMemo causes visible input lag.
Even a 2ms filter becomes 200ms total if it fires 100 times during a single typing burst.
Always benchmark before and after – console.time is your cheapest profiling tool.
Key Takeaway
useMemo caches computed values, not just references.
Profile before you memoize – if computation <1ms, skip it.
Never rely on useMemo for side effects – React may discard the cache.
Deciding when to use useMemo
IfComputation involves large arrays (>1000 items) or heavy transformations
UseWrap with useMemo – the saved work outweighs the overhead
IfYou need a stable object/array to pass as prop to React.memo child
UseUse useMemo even for trivial values to stabilise the reference
IfComputation is a simple addition or string concatenation (<1μs)
UseSkip useMemo – the hook's own execution will dominate

Production Patterns, Edge Cases, and the Hidden Costs of Over-Memoizing

Here's the uncomfortable truth: useMemo and useCallback cost something too. Every memoized value consumes memory. Every render still executes the hook, runs the dependency comparison, and decides whether to return the cached value or recompute. For cheap computations and simple primitives, that overhead can easily exceed what you saved.

Over-memoization is a real production pattern that makes codebases harder to read, harder to refactor, and sometimes actually slower. The React core team's recommendation is measured and specific: profile first, memoize where the profiler shows a real problem, and review your memoization regularly as component structures change.

There are three advanced patterns worth knowing. First, memoizing derived context values — if you provide an object through React Context without memoizing it, every context consumer re-renders on every provider re-render even if the values inside the object didn't change. useMemo on the context value object is one of the few cases where memoization pays dividends across a wide component tree. Second, stable refs as an alternative — useRef gives you a stable container for free, and for event handlers that need access to latest state without re-subscribing, combining useRef with useCallback can eliminate deps entirely. Third, the useEvent pattern (RFC stage) — a future hook that aims to give you a stable function reference that always closes over fresh values, solving the deps-vs-stability tension permanently.

ContextMemoPattern.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React, { useState, useCallback, useMemo, useContext, createContext } from 'react';

// ─── Context setup ────────────────────────────────────────────────────────────
const ShoppingCartContext = createContext(null);

export function ShoppingCartProvider({ children }) {
  const [cartItems, setCartItems] = useState([]);
  const [promoCode, setPromoCode] = useState('');

  const addItem = useCallback((item) => {
    setCartItems(current => [...current, item]);
  }, []); // stable: only uses the setter, no external deps

  const removeItem = useCallback((itemId) => {\n    setCartItems(current => current.filter(i => i.id !== itemId));
  }, []);

  const applyPromo = useCallback((code) => {
    setPromoCode(code);
  }, []);

  // ✅ Critical pattern: memoize the context VALUE object.
  // Without this, every render of ShoppingCartProvider creates a new
  // context object reference → ALL consumers re-render, even components
  // that only use `addItem` and haven't seen a cart change.
  const contextValue = useMemo(
    () => ({
      cartItems,    // changes when items are added/removed
      promoCode,    // changes when promo is applied
      addItem,      // stable reference from useCallback
      removeItem,   // stable reference from useCallback
      applyPromo,   // stable reference from useCallback
    }),
    [cartItems, promoCode, addItem, removeItem, applyPromo]
  );

  return (
    <ShoppingCartContext.Provider value={contextValue}>
      {children}
    </ShoppingCartContext.Provider>
  );
}

// ─── Consumer that only needs the addItem action ──────────────────────────────
const AddToCartButton = React.memo(({ productId, productName }) => {\n  // This component subscribes to the full context, which is why\n  // the memoized context value above is so important.\n  // If contextValue changes reference on every render, this re-renders too.\n  const { addItem } = useContext(ShoppingCartContext);

  console.log(`AddToCartButton rendered for: ${productName}`);

  return (
    <button onClick={() => addItem({ id: productId
Output
// With memoized context value:
// cartItems changes (item added) → contextValue updates →
// → AddToCartButton re-renders (subscribed to context) ✅ expected
// promoCode changes → contextValue updates →
// → AddToCartButton re-renders only if it uses promoCode
// → With split contexts (cartItems vs actions), you can isolate this further
// Parent of ShoppingCartProvider re-renders for unrelated reason →
// → contextValue reference is UNCHANGED (useMemo deps unchanged)
// → AddToCartButton does NOT re-render ✅ memoization working
Watch Out: Stale Closures in useCallback
If you omit a dependency from useCallback's dep array to keep the function stable, you'll get a stale closure — the function silently reads an old value of that variable forever. This is a runtime logic bug, not a crash, making it one of the hardest bugs to find. Always use eslint-plugin-react-hooks with exhaustive-deps: 'error' in your project config. Trust the linter over your intuition here.
Production Insight
Context value objects recreated on every render cause every consumer to re-render – even those that don't use the changed data.
useMemo on the context value is one of the highest-leverage applications of memoization.
Combine with useCallback to keep action references stable and prevent all unnecessary consumer re-renders.
Key Takeaway
Memoize context values to prevent cascading re-renders.
Stale closures are the hardest bugs – trust exhaustive-deps.
The useRef pattern gives you stable callbacks that always read the latest state.
Choosing between useRef + useCallback and just useCallback
IfCallback depends on frequently changing state that is not needed for comparison
UseUse useRef to hold latest value, then stable useCallback with empty deps
IfCallback depends on props/state that change infrequently
UseInclude those values in deps directly – the re-creation cost is negligible
IfYou are providing a value object through React Context
UseAlways wrap the context value in useMemo to prevent cascading re-renders

Diagnosing Memoization Failures — A Step-by-Step Debugging Workflow

When memoization doesn't work as expected, it's rarely because the hooks themselves are broken. The cause is almost always one of: an unstable dependency, a missing React.memo on the child, or a stale closure. Here's a systematic way to diagnose which one.

Start with React DevTools – locate the memoized child in the component tree and verify it's actually rendered as memo. If the child re-renders when you expect it not to, record a profile and look at the props panel for the child. The 'changed' markers show exactly which prop reference changed.

Next, if a prop that should be stable is flagged as changed, examine its origin in the parent. If it's an inline function, wrap it with useCallback and check that the dependencies are correct. If the prop is an inline object or array, wrap it with useMemo. If the prop is already in useCallback/useMemo but still changes, the culprit is likely a dependency that itself is unstable – an object defined inline inside the dependency array's scope.

Sometimes you need to debug at a lower level. Use a useEffect with console.log of the dependency array values to see when they actually change. This is especially useful for detecting hidden reference changes.

Finally, the stale closure case is the trickiest. If your callback uses a variable from the enclosing scope but you've deliberately omitted it from deps to keep the function stable, the callback will always see the initial value. Use the ref pattern to hold the latest value without triggering deps changes.

DebugWorkflow.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Utility to log when a useCallback is actually recreated.
// Add this inside the component to debug dep changes.
function useDebugDeps(label, deps) {\n  const prevDepsRef = React.useRef(deps);\n  const depsChanged = prevDepsRef.current !== deps && !deps.every((dep, i) => Object.is(dep, prevDepsRef.current[i]));\n  useEffect(() => {\n    if (depsChanged) {\n      console.log(`${label} deps changed:`, { prev: prevDepsRef.current, current: deps });
    }
    prevDepsRef.current = deps;
  });
}

// Usage in component:
const handleRemove = useCallback(() => {
  doSomething(userId);
}, [userId]);
useDebugDeps('handleRemove', [userId]);

// Now every time handleRemove is recreated, you'll see what dep changed.
Output
// Console output when userId changes:
handleRemove deps changed: { prev: [1]
Pro Tip: Use a Custom Hook for Dependency Debugging
The useDebugDeps hook shown above is invaluable in complex components. It logs the exact deps that changed and their previous values. It's your first line of defense when useCallback or useMemo behave unexpectedly.
Production Insight
In a production incident, a team spent two days debugging a useCallback that appeared correct but still caused re-renders.
The root cause was a parent component's prop that was an inline array passed as a React component prop.
Lesson: always check the entire prop chain for reference instability – not just the immediate useCallback.
Key Takeaway
Use React DevTools to identify which props changed.
A changing useCallback/useMemo usually points to unstable deps.
The ref pattern solves stale closures without sacrificing stability.
Debug workflow decision tree
IfReact.memo child re-renders, but no prop changed visually
UseCheck context or parent's state that may affect child indirectly
IfReact DevTools shows prop changed, but it's wrapped in useCallback
UseInspect whether the deps include an unstable object – use useDebugDeps
IfNo prop changes but child still renders (no React.memo)
UseAdd React.memo – you can't skip renders without it

The Three-Hook Trifecta — When to Reach for Each One

UseCallback, useMemo, and useEffect are not interchangeable. New devs reach for useCallback like a hammer. Respect each hook's job.

useCallback exists for one reason: referential stability of function references. If you pass a callback to a child wrapped in React.memo, you need useCallback. Otherwise, you're forcing a re-render on every parent update with a brand new function object. No function logic invocation happens here — just reference caching.

useMemo caches computed values. Expensive calculations? Data transformations? Filtering large arrays? That's useMemo territory. It runs the function only when dependencies change. But if your computation is cheap — a string concat or a simple map — skip it. The caching overhead costs more.

effect handles side effects: API calls, subscriptions, DOM mutations. It fires after render completes. Don't mix effect with useMemo for computation. Don't use useCallback inside effect for cleanup functions unless the effect depends on changing props.

Rule: useCallback for stable references. useMemo for expensive computations. useEffect for side effects. Mix them only when you understand the render-priority chain.

HookSelector.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge
import { useMemo, useCallback, useEffect, useState } from 'react';

function DataGrid({ items, onItemClick }) {
  const [filter, setFilter] = useState('');

  // Expensive: useMemo
  const filteredItems = useMemo(
    () => items.filter(item => item.name.includes(filter)),
    [items, filter]
  );

  // Stable reference: useCallback
  const stableClick = useCallback(
    (id) => onItemClick(id),
    [onItemClick]
  );

  // Side effect: useEffect
  useEffect(() => {
    console.log(`Filter changed to: ${filter}`);
  }, [filter]);

  return <List items={filteredItems} onItemClick={stableClick} />;
}
Output
Filter changed to: ''
Production Trap:
Never wrap a useEffect callback in useCallback. The effect function runs after render, not on mount. If you need to skip effect runs, control with dependencies — not memoization.
Key Takeaway
useCallback for references, useMemo for values, useEffect for side effects. Don't mix the jobs.

Dependency Arrays — The Silent Performance Killer

Every memoization hook relies on a dependency array. Get it wrong and you're debugging stale closures or over-memoizing yourself into a corner.

Missing a dependency breaks referential equality. If your useCallback depends on data but you omit it from the array — the callback captures the initial value forever. Your child component sees the same stale function reference and never updates. The bug looks like a missing re-render.

Too many dependencies defeats the purpose. If everything changes on every render because you included the entire props object — congratulations, you've added overhead for zero gain. The memoized wrapper runs every time, matching the cost of no memoization plus the diff check.

Rule of thumb: include only the variables your function or computation actually reads. Use ESLint's react-hooks/exhaustive-deps plugin. It's not optional. It catches 90% of stale closure bugs before they hit production.

One more trap: objects and arrays created inline. { id: 1 } creates a new reference every render. If that's in your dependency array, you'll trigger the memo on every render. Use useMemo or a stable reference outside the component.

DependencyPitfall.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge
import { useCallback, useState } from 'react';

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  // STALE BUG: missing 'userId' dependency
  const fetchUser = useCallback(
    () => fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser),
    [] // <-- userId not included! Stale closure
  );

  // FIXED: explicit dependencies
  const fetchUserFixed = useCallback(
    () => fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser),
    [userId]
  );

  return <div>{user?.name}</div>;
}
Output
Error: userId is undefined on re-render with new userId
Production Trap:
ESLint react-hooks/exhaustive-deps is not a suggestion. It's a linter rule that prevents silent data corruption. If you suppress it, document why in a comment with the specific ESLint disable reason.
Key Takeaway
Dependency arrays are contracts. Keep them minimal, explicit, and lint-enforced.
● Production incidentPOST-MORTEMseverity: high

The Stale useCallback That Sent Wrong User IDs

Symptom
After user A logs out and user B logs in, all new messages are still sent as user A. The UI shows user B's name but the API payload contains user A's ID.
Assumption
useCallback with an empty dependency array is safe because the handler only uses setter functions, which are guaranteed stable by React.
Root cause
The callback closed over a userId variable from the component scope. The developer omitted userId from the deps array to keep the function stable, but the callback always captured the initial value of userId (user A's ID). When user B logged in, the handler still referenced user A's ID.
Fix
Include userId in the useCallback dependency array. If stability is critical and the dependency changes too often, use the ref pattern: const userIdRef = useRef(userId); useEffect(() => { userIdRef.current = userId; }, [userId]); then inside the callback use userIdRef.current instead of userId directly.
Key lesson
  • Always include every external variable used inside useCallback in the dependency array – trust exhaustive-deps.
  • When you cannot include a dependency without causing unwanted re-renders, use the ref pattern to capture the latest value without recreating the callback.
  • Treat exhaustive-deps warnings as blocking errors; stale closures are among the hardest bugs to find because they produce no error, only incorrect behavior.
Production debug guideCommon symptoms and how to diagnose them3 entries
Symptom · 01
Child component wrapped in React.memo still re-renders on every parent render.
Fix
Check if any prop is a new reference each render – use console.log comparison or breakpoint. Look for inline objects, arrays, or functions in the parent JSX. Open React DevTools Profiler to see which prop changed.
Symptom · 02
useCallback returns new reference even though deps haven't changed.
Fix
Identify if any dep is an object or array that is recreated inline. Use useMemo to stabilize those deps or move them outside the component. Log the dep values in a useEffect to detect hidden changes.
Symptom · 03
useMemo computes on every render despite unchanged deps.
Fix
Check if deps array contains an inline object or array – these are new on every render. Also ensure the computation is not needed on first mount; useMemo still runs on first call.
★ QuickDebug: useMemo/useCallback IssuesCommon symptoms and immediate actions to diagnose memoization problems in React
React.memo child re-renders unexpectedly
Immediate action
Open React DevTools Profiler and record a state change that should not affect the child.
Commands
Select child component in Components tab, check props tab to see which prop is marked as changed.
Add console.log as first line of the child component to observe when and why it renders.
Fix now
Wrap the unstable prop with useMemo or useCallback; if the prop is an inline object/array, move it outside the component or memoize it.
useCallback always returns a new function+
Immediate action
Check the deps array – does it contain an inline object or array?
Commands
Log the deps inside a useEffect to see when they actually change.
Use a stable ref to hold the value if the dep changes too often.
Fix now
If the dep is an object, memoize it with useMemo; if it's a primitive that changes unnecessarily, investigate why the upstream component creates new references.
Stale closure in event handler+
Immediate action
Compare useCallback deps with variables used inside the callback.
Commands
Add console.log inside the callback to verify which values are closed over.
Use a ref to capture the latest value without adding it to deps: const ref = useRef(value); useEffect(() => { ref.current = value; }, [value]);
Fix now
Either include the missing dependency in useCallback, or apply the useRef pattern to keep the callback stable while reading fresh values.
useMemo vs useCallback: Quick Comparison
Feature / AspectuseMemouseCallback
What it memoizesThe return value of a functionThe function reference itself
Equivalent touseMemo(() => value, deps)useMemo(() => fn, deps) — literally identical internally
Primary use caseExpensive computations, stable object referencesStable function references passed to memoized children
Return typeAny value (number, array, object, etc.)Always a function
When to useFilter/sort of large datasets, derived complex stateEvent handlers passed to React.memo children, useEffect deps
When NOT to useCheap computations, primitive values, non-memoized childrenFunctions not passed as props, no React.memo on child
Works with React.memo?Yes — stabilises object prop referencesYes — stabilises function prop references
Risk of misuseWasted memory on trivial valuesStale closures from missing dependencies
Concurrent Mode safe?Yes, but React may discard cache — never use for side effectsYes — same rules apply
Production priorityMedium — profile firstMedium — profile first, check React.memo is actually on child

Key takeaways

1
useCallback and useMemo don't make code faster by themselves
they preserve referential identity so that React.memo on child components can actually bail out of re-rendering. Without React.memo on the child, both hooks are burning memory for zero gain.
2
useCallback(fn, deps) is literally useMemo(() => fn, deps) under the hood
internalising this makes both hooks feel like one concept rather than two, and helps you reason from first principles instead of memorising rules.
3
Stale closures from missing useCallback dependencies are runtime logic bugs, not crashes
they're the hardest category of React bug to find. Enable eslint-plugin-react-hooks with exhaustive-deps: 'error' and treat every warning as a blocking issue.
4
Memoizing a context value object is one of the highest-leverage applications of useMemo in a real app
without it, every context consumer re-renders on every provider render regardless of whether the relevant values changed, silently degrading performance across the entire tree.
5
Profile before memoizing and after removing memoization. Use React DevTools Profiler and console.time to measure actual impact. If you can't measure a difference, the memoization isn't doing useful work.

Common mistakes to avoid

3 patterns
×

Memoizing everything 'just in case'

Symptom
Every function is wrapped in useCallback and every computed value in useMemo, even single-line arithmetic. Code is hard to read, memory usage grows, and performance shows zero gain.
Fix
Open React DevTools Profiler, find components that actually re-render unexpectedly or compute slowly, and memoize only those. If console.time shows less than 1ms, skip useMemo entirely.
×

Applying useCallback without React.memo on the child

Symptom
useCallback stabilises the function reference, but the child re-renders on every parent render anyway because the child isn't wrapped in React.memo.
Fix
Always pair useCallback on a prop with React.memo on the receiving component. They only work as a pair. Without React.memo, useCallback adds overhead but provides zero re-render prevention.
×

Omitting dependencies to force stability (stale closure bug)

Symptom
The callback always reads the initial value of a piece of state, so user interactions appear to have no effect or produce stale data – no error is thrown, making it extremely hard to detect.
Fix
Never manually trim deps to force stability. Use the functional update form of setState (setCount(c => c + 1)) to avoid needing state in deps, or use the useRef 'latest ref' pattern when you genuinely need a stable callback that reads the latest value.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between useMemo and useCallback, and can you expla...
Q02SENIOR
I have a child component wrapped in React.memo and I'm passing a callbac...
Q03SENIOR
You're working on a large e-commerce app and a colleague has wrapped eve...
Q01 of 03SENIOR

What's the difference between useMemo and useCallback, and can you explain why useCallback(fn, deps) is technically equivalent to useMemo(() => fn, deps)? Walk me through what that means at the memory level.

ANSWER
useMemo caches the return value of the factory function you pass to it. useCallback caches the function itself. They are equivalent because useCallback(fn, deps) is a convenience wrapper that calls useMemo(() => fn, deps). At the memory level, both store a reference in React's internal fiber cache: useMemo stores the value returned by fn, useMemo(() => fn) stores the function object fn. So useCallback avoids creating an extra wrapper function (the arrow function) even though internally the two calls are identical. In terms of memory usage, useCallback simply stores the function object reference; useMemo stores whatever the factory returns (could be a function, object, etc.). The practical difference is semantic: useCallback signals to the reader that you are intentionally preserving a function reference; useMemo signals that you are preserving a computed value.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Does useCallback make my function run faster?
02
Should I put every function inside useCallback?
03
Why does my useCallback still return a new function even though my deps didn't change?
04
Can I use useMemo for side effects?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
June 10, 2026
last updated
1,554
articles · all by Naren
🔥

That's React.js. Mark it forged?

8 min read · try the examples if you haven't

Previous
React useContext and useReducer
6 / 47 · React.js
Next
React Router