A React component calls useEffect(() => { load(options); }, [options]) where const options = { page } is built in the component body. The effect re-runs on every render even when page never changes. Why?
answer
- React never looks inside a dependency
- identity, not contents
- the component body runs every render
- fresh literal, fresh reference
- prefer primitives in the array
basics
~20 sReact compares each dependency to the previous render's value with Object.is, and an object literal built during render is a brand-new reference every time, so the comparison always fails. Depend on the primitive page, or on a stable reference.
solid answer
~50 sReact does not look inside your dependencies. After each render it compares the new array to the stored one position by position using `Object.is`, which for objects is a reference check. `{ page }` is evaluated during every render, so each render produces a different object — same contents, different identity — and `Object.is(prevOptions, options)` is always `false`. The effect therefore re-runs every time, and if it sets state you get a loop. Three honest fixes: list the primitive instead of the wrapper, so the dependency is `[page]`; hoist the object out of the component when it is genuinely constant; or wrap it in `useMemo` keyed on the same primitives when something else really does need the object identity. The first is usually the right one — the effect depends on `page`, not on the box you put it in.
code
javascript · 18 linesimport { useEffect } from 'react';
function Broken({ page, load }) {
const options = { page }; // new reference every render
useEffect(() => {
load(options);
}, [options]); // Object.is is always false -> runs every render
return null;
}
function Fixed({ page, load }) {
useEffect(() => {
load({ page }); // built inside; identity is irrelevant here
}, [page, load]); // page is a number, compared by value
return null;
}
export { Broken, Fixed };go deeper
Know that objects and arrays created inside a component are recreated on every render, and that React compares dependencies by reference. Say plainly that listing page instead of { page } fixes it.
Explain the comparison itself — positional, per item, using Object.is — and walk the render-to-effect-to-setState loop that an unstable dependency produces. Name the fixes in order: primitive dependency, module-level constant, then deliberate memoization.
Diagnose it from symptoms rather than from the code: repeated requests, an effect run count outpacing renders, and a dependency whose identity changes while its contents do not. Explain why deleting the dependency to stop the noise trades a loop for a stale bug.
Own the convention: dependency arrays stay honest and comparable by design, which usually means shaping data so effects depend on primitives. Be ready to argue when memoized identity is worth its maintenance cost and when the real answer is to move the work out of effects.
## What the dependency check actually is Every time a component renders, React compares the dependency array you just passed with the one it stored from the previous render for that hook slot. The comparison is positional and per item, and the predicate is `Object.is`. For primitives that behaves like `===` with two refinements — `NaN` equals `NaN`, and `+0` does not equal `-0`. For objects, arrays and functions it is pure reference identity: two values are the same only when they are literally the same object in memory. There is no deep comparison anywhere in this path. React never walks the object's properties, and it never inspects the effect body to work out what you meant. ## Why the object dependency always differs A component body runs top to bottom on every render. That means this line runs again each time: ```js const options = { page }; ``` The object literal is an expression, so it allocates a fresh object per render. `page` may be `3` on ten consecutive renders and the ten `options` objects will still be ten distinct references. `Object.is` compares them pairwise and returns `false` every time, so React tears down and re-runs the effect on every commit — the same behaviour as if you had omitted the dependency array entirely. The failure mode escalates fast when the effect writes state. Render creates a new `options`, the dependency check fails, the effect runs, `load` resolves and sets state, the state change triggers a render, which creates a new `options`… That is the classic infinite-request loop, and its root cause is identity, not the fetching. ## The same trap in three disguises Anything constructed during render has this property: ```js useEffect(fn, [{ page }]); // new object each render useEffect(fn, [items.filter(Boolean)]); // new array each render useEffect(fn, [() => save(id)]); // new function each render useEffect(fn, [style]); // new object, if style is built inline ``` An inline arrow is the most commonly missed one, because it does not look like data. A callback prop received from a parent that builds it inline has exactly the same problem — the identity changes in the parent, and your dependency array faithfully reports the change. ## Fix one: depend on the primitives The strongest fix is usually to stop depending on the wrapper: ```js useEffect(() => { load({ page }); }, [page]); ``` The object is now built inside the effect, where its identity is irrelevant, and the dependency is a number that compares by value. This is worth internalising as a habit: **prefer primitive dependencies**. They compare correctly for free and they say exactly what the effect reacts to. ## Fix two: hoist what is constant If the object does not depend on anything from the render, move it out of the component entirely: ```js const DEFAULT_OPTIONS = { retries: 3 }; function Widget() { useEffect(() => load(DEFAULT_OPTIONS), []); } ``` A module-level constant has one identity for the process lifetime, so it can never destabilise a dependency array. ## Fix three: stabilise the identity deliberately When something genuinely needs the object — you pass it onward, or it is expensive to construct — give it a stable identity with `useMemo` keyed on the primitives it derives from, and then depend on the memoized value. That is a real tool, but treat it as the third choice: it adds a second dependency array you now have to keep honest, and it moves the problem rather than removing it. ## Why React chose identity over deep equality Deep comparison would have to run on every render for every dependency, its cost would scale with the data rather than the number of dependencies, and it would still be wrong for functions. Identity is O(1), predictable, and matches how JavaScript itself defines sameness. The tradeoff React makes is that keeping dependencies comparable is the author's job — which is exactly what this question is testing. ## Diagnosing it in the wild The tell is an effect that runs far more often than the data changes, usually noticed as repeated network calls or a log line that never stops. Log the dependency alongside a render counter, or compare identities directly: if `Object.is` on consecutive values of a dependency is `false` while the rendered output looks unchanged, you have found it.
- Would wrapping the effect's work in a check like JSON.stringify(options) as the dependency be a reasonable fix?It works in the narrow sense — the string is a primitive and compares by value — but it is a poor habit. Serialization cost runs on every render, key order changes the string, and values like functions, undefined and Dates do not survive it, so you can get both false re-runs and missed ones. Depend on the primitives themselves instead.
- An inline arrow function passed down from a parent is in the dependency array. Why does that behave the same way?A function expression is an object, so a parent that creates it inline produces a new identity on every one of its renders. Your dependency check compares references and correctly reports a change. Either derive the effect's inputs from primitives, or have the parent give the callback a stable identity before it becomes a dependency.
- How would you confirm at runtime that identity is the cause rather than the value genuinely changing?Keep the previous value in a ref and log `Object.is(previous, current)` alongside the rendered value each time the effect runs. If the comparison is false while the displayed contents are identical, the dependency is being recreated. A counter of effect runs versus renders makes the ratio obvious in a few seconds.
saying these in an interview costs you the question
- Believes React compares dependency objects by their contents
- Adds the object to the array again to force stability
- Removes the dependency to stop the loop instead of fixing identity
- Thinks the object is only created once per component
- Reaches for useMemo before trying a primitive dependency