Senior 6 min · March 05, 2026

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
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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.
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.

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
● 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?
🔥

That's React.js. Mark it forged?

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

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