A page keeps an array of selected user objects and tests membership with selected.includes(user). After the user list is refetched from the server, the check starts returning false even though the same person is on screen. What is happening, and how do you fix it?
answer
- the array is fine, the comparison is not
- objects compare as references
- a refetch allocates new objects
- identical fields are still two values
- search on a stable id instead
basics
~20 sArray.prototype.includes compares objects by reference, not by content. A refetch builds brand-new objects, so the stored one and the fresh one are different values even with identical fields. Search by a stable field instead, with some or find and a predicate.
solid answer
~50 s`includes` and `indexOf` compare values, and for objects that means reference identity — two objects with identical properties are still two distinct values. While everything came from one fetch the stored reference and the rendered reference were the same object, so the check worked; after a refetch (or a `JSON.parse` round trip, or any mapping step that rebuilds the rows) the rendered objects are new allocations and no stored reference matches. The fix is to stop comparing object identity and compare a stable domain key: `selected.some(u => u.id === user.id)`, or `find`/`findIndex` when you need the row or its position. The more robust version is to store the ids rather than the objects, so the selection cannot go stale when the data reloads. Reference equality is only safe when you control that exactly one instance per entity exists for the lifetime of the check.
code
javascript · 12 linesconst fetched = [{ id: 7, name: 'ada' }];
const selected = [{ id: 7, name: 'ada' }]; // same data, rebuilt after a refetch
console.log(selected.includes(fetched[0])); // false — different references
console.log(selected.some(u => u.id === fetched[0].id)); // true — compares the key
const row = selected.find(u => u.id === 7);
console.log(row.name); // 'ada'
// Sturdier: keep ids, which compare by value
const selectedIds = selected.map(u => u.id);
console.log(selectedIds.includes(fetched[0].id)); // truego deeper
Know that two objects with identical properties are still different values, so includes only matches when it is handed the very same object reference. Say that searching by an id with find or some is the way around it.
Explain the mechanism: includes uses SameValueZero and indexOf strict equality, and for objects both reduce to reference comparison. Show the predicate form, selected.some(u => u.id === user.id), as the replacement.
Diagnose it out loud: the check works within one fetch and fails after any step that rebuilds the rows. Confirm by comparing a.id === b.id against a === b, then move the selection to stable ids so a reload cannot invalidate it.
Own the state-shape decision: settle that shared collections store entity keys rather than object references, so identity never depends on which fetch produced a row, and stale copies are not pinned in memory across reloads.
## The mechanism `Array.prototype.includes` compares the search argument against each element using **SameValueZero**, and `indexOf` uses strict equality. For primitives these compare contents. For objects, both algorithms compare **references**: two object values are equal only when they are literally the same object in memory. ```js [{ id: 7 }].includes({ id: 7 }); // false — two different objects const u = { id: 7 }; [u].includes(u); // true — same reference ``` Nothing in the language performs a structural comparison of objects for you. There is no deep-equality operator, and no array search method takes one. ## Why the bug appears only after a refetch While the page held a single fetch result, both the array of all users and the array of selected users pointed at the *same* objects, so identity comparison happened to agree with the intent ("is this person selected?"). Any step that rebuilds the rows breaks that coincidence: - a second network call, which parses fresh JSON into fresh objects; - a `JSON.parse(JSON.stringify(x))` round trip; - a mapping step such as normalising or adding a display field, which produces new objects; - a copy taken to avoid mutating the original. After any of these, `selected` holds references into the *old* graph and the rendered rows are the *new* graph. Every membership test fails, and it fails silently — no error, just a selection that appears to reset itself. This is the classic "works on first load, breaks after refresh" report. ## The fix: compare a stable key Swap the value comparison for a predicate over a field that identifies the entity across reloads: ```js // before — identity comparison selected.includes(user); // after — comparison by domain key selected.some(u => u.id === user.id); ``` `some` when you want a boolean, `find` when you want the stored row, `findIndex` when you need the position in order to remove it. The predicate methods exist precisely because value comparison cannot express "the same entity". The sturdier design goes one step further and does not keep the objects at all: ```js const selectedIds = [7, 12]; selectedIds.includes(user.id); // primitives compare by value — refetch-proof ``` Now the selection holds ids, `includes` compares numbers by value, and reloading the data cannot invalidate it. It also stops the selection from pinning stale copies of rows in memory. When the collections get large, keeping the ids in a `Set` and asking it for membership avoids rescanning an array on every render. ## What not to reach for The tempting shortcut is `JSON.stringify(a) === JSON.stringify(b)`. It is fragile: key order changes the output, `undefined` values and functions are dropped, `NaN` and `Infinity` become `null`, and dates become strings — so it can report a difference where there is none, or the reverse. If you genuinely need structural comparison, write a comparison that names the fields that matter for your domain; that is also self-documenting about which differences count. Also beware of the intermediate half-fix: adding a deep-equality helper inside a `some` predicate makes the check work again, but it compares *all* fields, so an unrelated server-side change to one field silently drops the row out of the selection. Identity is a domain question, and the domain answer is nearly always "the id". ## How to diagnose it quickly When a membership check misbehaves, the fastest confirmation is to compare a candidate pair directly: if `a.id === b.id` is `true` while `a === b` is `false`, you are comparing identities across two object graphs. Confirming that takes seconds and rules out the more exotic explanations. A second tell is that the bug reproduces only after a reload or a background refresh, never within a single render pass. ## The general rule Reference equality answers "is this the same instance?" Domain code almost always wants "is this the same entity?" Those coincide only while a single instance per entity is guaranteed for the whole lifetime of the comparison — a guarantee that a network refresh, a clone, or a normalisation pass quietly revokes. Store keys, compare keys, and keep object identity for cases where instance-level sameness is genuinely what you mean.
- Why does `includes` work correctly for an array of strings or numbers in the same situation?Primitives are compared by value, so two separately parsed copies of the string `'ada'` or the number `7` are the same value and match. Only objects and arrays compare by reference, because each literal or parse creates a distinct object. That is the core reason for storing ids rather than rows: it moves the membership test onto primitives, where a refetch cannot change the outcome.
- Is `JSON.stringify(a) === JSON.stringify(b)` a reasonable stand-in for object comparison here?No. It depends on property order, drops `undefined` values and functions, turns `NaN` and `Infinity` into `null`, and converts dates to strings, so it can report a difference that does not exist or miss one that does. If you need structural comparison, compare the specific fields that define sameness in your domain — usually just the id — which is both correct and self-documenting.
- How would you remove the matching row from the selection once you have switched to a predicate?Use `findIndex` with the same key predicate and act on the position it returns, guarding against the `-1` miss. If you keep ids instead of objects, removal is a plain filter over primitives. Either way, do the removal by key rather than by passing the object itself to an identity-based lookup, which reintroduces the original bug.
- When is comparing objects by reference actually the right thing to do?When instance-level sameness is what you mean: checking whether a listener callback is the one you registered, whether a cache entry is the exact object you stored, or whether two variables alias the same mutable state. Those are legitimately identity questions. The rule of thumb is that identity is right when you own every instance and no copy or reparse can occur between storing and comparing.
Two printed copies of the same passport carry the same details, but stapling one into a folder does not put the other in it — a filing check that asks "is this exact sheet in the folder?" says no, while one that asks "is this passport number in the folder?" says yes.
saying these in an interview costs you the question
- Assumes includes compares object contents
- Says the array must have been mutated somewhere
- Reaches for JSON.stringify as an equality check
- Adds deep equality over all fields instead of the id
- Concludes the server returned different data