Why does a fresh object built during every render read as changed to every identity check downstream?
answer
- new allocation, same contents
- identity churn, not data change
- only hurts where a seam compares
- hoist, split into primitives, or cache
- effect plus write equals a loop
basics
~20 sEach pass allocates a different object, and an identity check compares which object it is, not what it holds. Identical contents in a new wrapper report a change, so dependency lists re-fire and children that could skip work do not.
solid answer
~50 sAn object literal, array literal or function created inside a render is a **new object on every pass**. Downstream seams compare by reference, so they see a different value each time and report a change even though every field holds what it held before — a child allowed to skip work when its inputs are unchanged never skips, a dependency entry looks new on every pass, and a cache keyed on that input never hits. Nothing is *wrong* in the output; the work is simply needless, and in the effect case it can loop, because the work writes state, the write starts a pass, and the pass rebuilds the object. The fix is to stop producing a new value: hoist it out of the render path, hand over primitives instead of a wrapper, keep it in state, or build it only when its inputs change.
go deeper
Remember that an object or function written inline is created again on every pass, so anything comparing by reference sees something new even when the fields are identical.
Explain which seams actually care — a child that may skip, a dependency list, a cache key — and name the fixes: hoist, pass primitives, keep it where it survives passes, or build it only when inputs change.
Show judgment about when not to act: identity churn with no seam behind it costs one allocation. And be able to trace the loop where an effect's write rebuilds its own dependency.
Treat input stability as part of a shared module's contract: consumers will memoise and will list your values as dependencies, so which values a module promises to keep stable is an API decision, not an implementation detail.
## What "unstable identity" means Every time a component's body runs, the literals in it are evaluated again. `{ page: 1, size: 20 }` written inline is not one object that happens to be reused — it is a fresh allocation per pass, holding the same field values as its predecessor. The same is true of an array literal, and of a function written inline: a function is an object too, and a new one each pass. A seam that compares by reference answers only "same object or not". Identical contents in a different wrapper therefore read as **changed**. The value is honest about nothing having changed; the comparison cannot see that. ## Where it actually hurts It only hurts where a seam compares: - **A child that may skip work when its inputs are unchanged** receives a new object every pass and so never skips. The skip was the whole point of putting the comparison there. - **A dependency list** compares entry by entry against the previous pass's list. One freshly built entry makes the list look different on every pass, and the dependent work re-runs every time. - **A cached computation** keyed on that value recomputes on every pass, so the cache costs bookkeeping and returns nothing. - **An effect that also writes state** can loop: the write starts a new pass, the pass rebuilds the dependency object, the object looks new, the effect runs again. This is the failure mode that shows up as a spinning page or a repeating request rather than as a harmless waste. Where nothing downstream compares by identity, an inline object costs one allocation and no more. Chasing stability everywhere is its own waste — the question to ask is always "is there a seam here?". ## Why comparing harder is the wrong first move The tempting fix is to make the seam compare contents instead of identity. It works, and it moves the cost: a content comparison runs on every check and grows with the value, while the identity check it replaced was a single test. It also does not remove the new object — anything else keyed on identity still sees churn. The cheaper move is upstream: stop handing over a new value when nothing changed. | symptom | cause | the honest fix | |---|---|---| | child never skips its work | inline object or inline function passed as an input | hoist, cache against real inputs, or pass primitives | | dependent work re-runs every pass | a freshly built entry in the dependency list | depend on the primitive fields, not on the wrapper | | repeating work that writes state | the write triggers the pass that rebuilds the dependency | break the cycle at the producer, not by removing the dependency | | cache never hits | its input is rebuilt each pass | stabilise the input first, then the cache is meaningful | ## Four ways to stabilise a value 1. **Hoist it.** A value that does not depend on state or props is a constant; define it once outside the render path and every pass hands over the same object. 2. **Pass primitives.** Numbers, strings and booleans compare by value, so two separate passes carrying `page = 1` and `size = 20` compare equal with no wrapper to keep stable. Splitting a small config object into two inputs is often the whole fix. 3. **Keep it in state, or in a stable container.** A value stored where it survives passes is the same object next pass by construction. This is the right answer for values that must persist, such as a controller or a collection built once. 4. **Build it only when its inputs change.** Cache the construction against the values it derives from — and check that those inputs are themselves stable, or the cache has merely moved the problem one level up. A note on the second option: stabilising a value whose contents genuinely change every pass is not stabilisation, it is a bug. Holding on to the old object while the data behind it moves produces a screen that shows stale values — the invisible-change failure, reached from the other direction. ## What differs by reactivity model The problem belongs to models where identity is the signal. In a runtime that re-runs the component and compares, unstable inline values are a constant background concern and the discipline is learned early. In a runtime with fine-grained read tracking, most of these seams do not exist: dependencies come from reads at the point of use rather than from a list of values compared between passes, so an inline object matters only where you deliberately hand it to something that compares. In a compile-time model, the compiler often knows which values can change and can keep the others stable for you. The concept to carry between all of them is the same: a seam that compares by identity is only as useful as the stability of the values you feed it.
- Why is passing two primitives often better than stabilising one options object?Primitives compare by value, so nothing has to be kept stable: two passes carrying the same number compare equal by construction. It removes the seam's dependence on a producer's discipline, and it removes the cache or hoist you would otherwise have to maintain and get right.
- Is an inline object always a problem?No. It costs one allocation, and that is all, unless something downstream compares it by identity — a child that may skip work, a dependency list, a cache key. Stabilising values that nothing compares adds code and bookkeeping for no gain, so look for the seam before reaching for a fix.
- How do you tell identity churn from a genuine change while debugging?Hold the previous value at the seam and compare the two facts separately: whether the identity differs, and whether the contents differ. New identity with identical contents is churn from the producer; identical identity with different contents is a mutation. Both look like "the comparison is wrong" until you split them.
saying these in an interview costs you the question
- Thinks two object literals with identical fields compare as equal.
- Reaches for a content comparison at the seam before checking the producer.
- Stabilises a value whose data genuinely changes, and ships stale output.
- Removes a dependency entry to stop a loop instead of stabilising it.
- Assumes an inline function is cheap because it holds no data.