In React, you wrapped a handler in useCallback but the child still receives a brand-new function on every render. What comparison does React run on the dependency array, and what usually defeats it?
answer
- compared per entry, not deeply
- positional walk over the old array
- Object.is on each dependency
- fresh object literals never compare equal
- stabilize upstream or depend on primitives
basics
~20 sReact compares each dependency positionally against the previous render's array using Object.is, and returns the new function if any entry differs. A dependency allocated fresh each render — an inline object, array or unmemoized callback — never compares equal, so the cache always misses.
solid answer
~50 sReact stores the dependency array from the last render next to the cached function. On the next render it walks the new array positionally and compares each entry to the old one with `Object.is` — a shallow, per-entry identity check, not a deep comparison. If every entry matches it returns the stored function; if any entry differs it stores the new one and returns that. So a dependency that is a freshly allocated object, array or arrow literal is a different reference every render, and the memo misses every time. The usual culprits are an inline options object, an array literal, or a callback prop the parent did not stabilize. The fix is upstream: depend on primitives pulled out of the object (`options.id` rather than `options`), hoist genuinely constant values out of the component, or stabilize the offending value where it is created. Note the arrow you pass is still allocated on every render regardless — `useCallback` only chooses which function to hand back.
code
javascript · 15 linesimport { useCallback } from 'react';
function Panel({ userId, mode, fetchRows }) {
const opts = { userId, mode };
// never cached: `opts` is a new object on every render
const bad = useCallback(() => fetchRows(opts), [opts, fetchRows]);
// cached while the primitives hold: strings and numbers compare by value
const good = useCallback(
() => fetchRows({ userId, mode }),
[userId, mode, fetchRows]
);
return null;
}go deeper
Know that the dependency array is compared entry by entry by identity, and that an object or array written inside the component is a new value each render, so listing it as a dependency defeats the cache.
Name Object.is as the comparison, describe the positional shallow walk over the previous array, and diagnose an unstable dependency from a code sample rather than guessing.
Show that you fix the instability at its source — depending on primitives, hoisting constants, or stabilizing the value in the parent that creates it — instead of stacking more wrappers around the symptom.
Treat it as an API-design signal: hooks and components that accept object or callback parameters push identity management onto every caller. Argue for parameter shapes that are stable by construction.
## What React stores and what it compares Every `useCallback` or `useMemo` call site keeps two things on the component instance: the last dependency array, and the last cached result. On each subsequent render React does one loop: - if the two arrays have different lengths, it cannot compare them meaningfully (React warns about this in development) and treats the cache as invalid; - otherwise it compares index 0 with index 0, index 1 with index 1, and so on, using `Object.is` on each pair; - if every pair matches, the stored result is returned and the factory is skipped; if any pair differs, the new result is stored and returned. Two properties of that loop matter. It is **positional** — index 0 of the new array is only ever compared with index 0 of the old — so a dependency list whose contents shift position between renders compares nonsense against nonsense. And it is **shallow**: `Object.is` on two distinct objects with identical contents is `false`. React never looks inside a dependency. ## Why a fresh allocation always loses An object literal, array literal or arrow function written inside the component body produces a brand-new value each time the body runs. Compare two of those with `Object.is` and you get `false`, every render, forever. So this shape can never hit its cache: ```js function Panel({ userId }) { const options = { userId, mode: 'compact' }; // new object every render const load = useCallback(() => fetchRows(options), [options]); // `load` is a different function on every render return null; } ``` The hook is doing exactly what it promised — `options` really did change identity — but the memoization is worthless because the dependency itself is unstable. This is the single most common reason a `useCallback` "does not work". The same happens with a dependency that is a callback prop the parent creates inline, or an array literal such as `[[a, b]]`, or a `new Date()` / `{}` default created in the body. ## Fixing it upstream, not downstream Adding another `useMemo` around the offending object sometimes helps, but the more robust fixes remove the instability instead: **Depend on primitives.** Strings, numbers, booleans, `null` and `undefined` compare by value under `Object.is`, so they are stable whenever their content is stable. Pull the fields you actually use out of the object and depend on those. ```js const load = useCallback(() => fetchRows({ userId, mode }), [userId, mode]); ``` This version reads more honestly too — it names exactly what the callback varies on. **Hoist constants out of the component.** A configuration object that never depends on props or state belongs at module scope, where it is allocated once for the lifetime of the module and is trivially stable. **Stabilize at the source.** If the unstable value arrives as a prop, the parent is where it must be memoized; wrapping it again in the child does not undo the fact that the prop changed identity. ## Object.is, precisely React uses `Object.is` rather than `===`. The difference shows up for two values only: `Object.is(NaN, NaN)` is `true` where `NaN === NaN` is `false`, and `Object.is(0, -0)` is `false` where `0 === -0` is `true`. So a `NaN` dependency does *not* invalidate the cache on every render, while flipping a dependency between `0` and `-0` does. These are edge cases, but they are the reason to say `Object.is` in an interview instead of "triple equals". ## The array itself must be stable in shape The dependency list has to be written literally, with a constant number of entries, in a constant order, on every render. Building it conditionally — `useCallback(fn, isAdmin ? [a, b] : [a])` — produces a size change React warns about in development and cannot compare reliably. If a value is only sometimes relevant, include it unconditionally and branch inside the function. ## The allocation happens either way A detail candidates often miss: `useCallback(() => doThing(id), [id])` still evaluates that arrow expression on every render. JavaScript creates the closure before the hook is even called; the hook then decides whether to keep it or discard it in favour of the stored one. `useCallback` never prevents a function from being created — it only controls which function identity escapes into your render output. The same is true of `useMemo`'s factory: the factory closure is allocated each render even when it is never called. ## Reading the failure in practice When a memoized value seems to change every render, the debugging move is to inspect the dependencies one at a time and ask which of them is a fresh allocation. It is almost always one entry, and almost always an object, array or function rather than a primitive.
- Why does React use Object.is here rather than the === operator?They agree everywhere except two values. `Object.is(NaN, NaN)` is `true`, so a `NaN` dependency does not falsely invalidate the cache on every render, while `===` would. And `Object.is(0, -0)` is `false`, so those are treated as distinct. It is the same comparison React uses to decide state bail-outs.
- What happens if the dependency array has a different number of entries between two renders?React cannot line the entries up positionally, so the cached value is not reused, and in development it warns that the array changed size. The array must be written literally with a constant length and order — if a value is only conditionally relevant, include it always and branch inside the function body.
- Does useCallback stop the arrow function from being created on each render?No. The arrow is a normal expression evaluated as the component body runs, so the closure is allocated before `useCallback` is even called. The hook only decides whether that new function or the stored one is returned. Wrapping never removes the allocation; it removes the identity change.
saying these in an interview costs you the question
- Thinks React deep-compares the dependency values
- Adds more dependencies to fix a cache that keeps missing
- Says useCallback prevents the function from being created
- Builds the dependency array conditionally or spreads a variable into it
- Assumes two objects with identical fields compare equal as dependencies