A store subscription uses a selector that builds a fresh object on each call; why does the component update on unrelated writes?
answer
- notifications go to everyone
- the reader decides a no-op
- identity comparison versus a fresh allocation
- select primitives, or compare the fields
basics
~20 sA subscription decides a notification is a no-op by comparing the new selector result with the previous one, usually by identity. A selector that builds a fresh object every call never returns the same identity, so every write anywhere looks like a change.
solid answer
~50 sA store's notification says only "something changed", and every registered listener gets it. The narrowing happens on the reading side: the subscription runs its **selector**, compares the result with the result it kept from last time, and treats an equal result as a no-op. The default comparison is identity, which is right for a number, a string or a boolean. A selector that assembles an object, maps a list or returns a tuple produces a structurally identical but freshly allocated result every call, so the comparison always says "changed" and the component updates on writes to fields it never reads — rendering identical output each time. Three ways out: select one primitive per subscription; give the subscription a comparison that inspects the fields instead of the identity; or hold the projection in the store so its identity changes only when its inputs do.
go deeper
Recall that a store notifies every listener and the selector decides whether that notification matters. Remember that a freshly built object is never equal by identity to the previous one.
Walk the five steps from write to scheduled update, and explain why identity comparison plus an allocating projection can never produce a no-op. Name the three fixes and what each costs.
Show how you diagnose it: compare consecutive selector results by identity and field by field, then decide whether the fix is narrower selection, a field comparison, or moving the projection into the store.
Set the convention. Decide whether subscriptions default to identity comparison with primitive selectors, and make the granularity policy explicit, so nobody reaches for a blanket deep comparison on the hottest path in the app.
## What a selector is for A store's notification carries no payload: it says "something changed", and it goes to **every** registered listener. A **selector** is the projection a subscriber applies to the store's state to answer a narrower question — the current user's display name, the count of unread items, whether a panel is open — so that subscriber can decide whether this particular change concerns it. The selector does not reduce the number of notifications. It converts a notification into a decision. ## The pipeline, step by step 1. A write is applied to the store. 2. The store calls **every** registered listener. 3. Each subscription runs its selector against the new state. 4. It compares the result against the result it kept from the previous run. 5. Equal means **no-op** — nothing is scheduled. Different means the component is scheduled to update, and the new result is kept for next time. Step 4 is where the bug lives. The default comparison is identity: is this the same value as before? For a primitive that is exactly right. For an object or a collection it asks whether it is the *same* object — and a selector that constructs its result constructs a different object every call. ## The fresh-object trap A selector written as `select(state) = { name: state.name, unread: state.unread }` returns something structurally identical to last time's result and never the same identity. Step 5 therefore always says "different", and the component updates after every write anywhere in the store, including writes to fields it does not read. The visible symptom is a component re-rendering constantly while producing identical output. The cause is neither the framework nor the store: it is a projection that allocates, compared by identity. The same trap catches a selector that maps, filters, sorts or slices — each of those produces a new collection — and one that returns a pair or appends a computed field. There are three honest ways out: - **Select one value per subscription.** Two subscriptions, each returning a primitive, wake independently and compare correctly by identity. - **Give the subscription a comparison that inspects the fields**, so a structurally identical projection counts as unchanged. The allocation is then wasted work rather than a wasted render. Which comparison is appropriate, and what it costs, is its own subject. - **Hold the projection in the store**, so its identity changes only when its inputs do, and let the subscriber select that one value. Right when the projection costs real work; how such a value is cached and invalidated is again its own subject. ## Granularity has two failure directions | What the selector returns | Wakes the component when | Failure mode | |---|---|---| | the whole state | any write | constant updates; memoising children only moves the cost downward | | a projection built per call | any write | the same, while looking narrow in review | | a projection with a field comparison | a read field changes | pays the comparison on every notification | | one primitive field | that field changes | many subscriptions to maintain | Too broad is the common failure. Too fine is real but rarer: one subscription per field means an action changing five fields wakes five subscriptions, and unless the store or the runtime coalesces them that can schedule several update passes for one logical change. It also adds listener bookkeeping that can outweigh the render it saved. ## Rules a selector should follow - **Read only from the state handed to it.** A selector that also reads a module variable, a clock or a value captured from an earlier render compares stale inputs, and its no-op decision stops being about the store. - **Do not allocate unless the caller compares structurally.** Identity comparison plus allocation is the combination that can never say no-op. - **Keep it cheap.** It runs once per subscription per notification, the hottest path the store has. - **Return the narrowest thing the component renders.** Selecting a container to read one field of it buys nothing. - **Never write.** A selector runs during notification, so a write from inside it re-enters the store mid-pass. ## Diagnosing "it re-renders for no reason" Capture what the selector returns on two consecutive notifications and compare the pair twice: once by identity, once field by field. Identical fields with different identity is the trap above, and the fix is on the reading side. Different fields means the change is real, and the question moves to whether this component should be reading that field at all — which is a question about where the value belongs, not about the subscription.
- Can a selector be too fine-grained?Yes. One subscription per field means a single action that changes five fields wakes five subscriptions, and unless the store or the runtime coalesces notifications that can schedule several update passes for one logical change. It also adds listener bookkeeping and comparison work that can cost more than the render it prevented.
- How do you let a component read several fields without waking on every write?Either compare the selected result field by field rather than by identity, or hold the projection inside the store so its identity only changes when its inputs do, and select that. A third option is one subscription per field, assembling the pieces in the component, which keeps identity comparison correct.
- Does a selector stop the store from notifying its subscriber?No. The store calls every registered listener on every write; it does not know which fields a selector reads. The selector runs inside the listener and decides whether the notification becomes an update. That is why selector cost matters: it is paid on every notification, not only on the ones that change something.
saying these in an interview costs you the question
- Believes a selector stops the store from notifying that subscriber.
- Returns a new object from a selector and blames the framework.
- Switches every subscription to a deep comparison to silence re-renders.
- Selects the whole state and relies on child memoisation instead.
- Reads a clock or module variable from inside the selector.