skip to content

In Vue 3, a reactive array holds plain objects; why does `list.includes(rawItem)` find one while `list.find(x => x === rawItem)` returns undefined?

level: seniorimportance: should knowfreq 35%

answer

  1. index reads return proxies
  2. some array methods are instrumented
  3. search runs on the raw array
  4. compare through toRaw or by id

basics

~20 s

Vue 3 instruments includes, indexOf and lastIndexOf to search the raw array, retrying with toRaw(arg), so a raw object is found. Callbacks such as find's receive reactive proxies, and a proxy never === the raw object.

solid answer

~40 s

Writes through a reactive array store raw objects, and reads through it return **reactive proxies** of them. For the identity-sensitive search methods — `includes`, `indexOf`, `lastIndexOf` — Vue substitutes instrumented versions that run the search on the raw array and, if that fails and the argument is a proxy, retry with `toRaw(arg)`; so both a raw and a proxied argument are found. `find`, `findIndex`, `some` and the other callback methods pass each item to your callback as a proxy, so `x === rawItem` is `false` every time, and so is `list[0] === rawItem`. Compare with `toRaw(x) === toRaw(rawItem)`, or better by a stable id. One edge: if the array was built from proxies before it became reactive, it holds proxies, and a raw lookup can miss.

code

ts · 16 lines
ts
import { ref, toRaw } from 'vue'

const a = { id: 1, name: 'Ada' }
const b = { id: 2, name: 'Lin' }
const list = ref([a, b])

list.value.includes(a)                    // true: searched on the raw array
list.value.indexOf(list.value[1])         // 1: proxy argument retried with toRaw
list.value[0] === a                       // false: index read returns a proxy
list.value.find(x => x === a)             // undefined: callback gets proxies
list.value.find(x => toRaw(x) === a)      // proxy of a
list.value.find(x => x.id === a.id)       // proxy of a, identity-free

const selected = ref<typeof a | null>(null)
selected.value = a
selected.value === a                      // false: ref returns the reactive proxy

go deeper

for a junior

Remember that items you read from a reactive array are proxies, so comparing them with === to the object you started with fails.

for a middle

Explain that includes, indexOf and lastIndexOf search the raw array and retry with toRaw, while callbacks and index reads receive proxies.

for a senior

Spot the raw-versus-proxy meeting points in real code, including a ref holding an object and arrays pre-filled with proxies, and fix them with id or toRaw comparisons.

for a principal

Make id-based comparison the team default for reactive collections so identity semantics never become a source of subtle selection bugs.

## What a reactive array stores and returns When you write an object into a reactive array (`list.push(item)`, `list[i] = item`), Vue's set handler stores the **raw** object: if you pass a proxy, it is unwrapped first. When you read an element through the proxy (`list[0]`, a `for...of` loop, a callback argument), Vue returns the element's **reactive proxy**, created lazily and cached per object. So the same logical item has two identities depending on which side of the proxy you stand: - inside the raw array: the plain object; - everything you read from `list`: a proxy that is never `===` to the plain object. The same holds for arrays inside a `ref`: `ref([a, b]).value` is a reactive array. ## The instrumented search methods Identity searches would break constantly under that rule, so Vue 3 intercepts them. For `includes`, `indexOf` and `lastIndexOf` it: 1. tracks the array's iteration, so the calling effect re-runs when elements change; 2. runs the native method **on the raw array** with your argument as given; 3. if nothing was found **and** the argument is a Vue proxy, runs it again with `toRaw(argument)`. Because the raw array holds raw objects, a raw argument is found in step 2, and a proxied argument is found in step 3. That is why `list.includes(rawItem)` and `list.indexOf(rawItem)` work. ## Where identity still fails | Code | Result for a raw `item` stored in `list` | Why | |---|---|---| | `list.includes(item)` | `true` | instrumented search on the raw array | | `list.indexOf(reactiveItem)` | the index | retried with `toRaw` | | `list[0] === item` | `false` | index reads return proxies | | `list.find(x => x === item)` | `undefined` | the callback receives proxies | | `list.findIndex(x => x === item)` | `-1` | same | | `plainArray.includes(list[0])` | `false` | a plain array knows nothing about proxies | | `selected.value === item` after `selected.value = item` on a `ref` | `false` | a ref holding an object returns its reactive proxy | The last row surprises people most: a ref that stores an object hands back that object's reactive proxy, so a 'currently selected' check against the object you assigned fails. ## The edge case where includes() misses The automatic retry only unwraps the **argument**. If the array was populated with proxies *before* it became reactive — `ref(items.map(x => reactive(x)))`, say — the raw array holds proxies, a raw argument is searched for and not found, and since the argument is not a proxy there is no retry. Keep raw data in, or search with the proxy. ## Fixes that hold up - **Compare by a stable key.** `list.find(x => x.id === item.id)` does not care about identity at all and is the most robust choice. - **Unwrap both sides.** `toRaw(x) === toRaw(item)` works whichever side is a proxy. - **Keep only proxies.** Take items from state and store those, never the raw object from before. - **Leave searches to `includes`/`indexOf`** when you genuinely need identity, since they already handle both forms. ## Why interviewers ask It separates candidates who know that Vue 3 reactivity is a wrapper — two identities per object — from those who only know the happy path. A good answer explains both why the search methods work and why callbacks and index reads do not.

  • In Vue 3, why can `list.includes(rawItem)` return false for an array created as `ref(items.map(x => reactive(x)))`?
    Those items were proxies before the array became reactive, so the raw array stores proxies. The instrumented search looks for the raw argument, does not find it, and only retries with `toRaw` when the argument itself is a proxy. Searching with the proxy, or storing raw objects, fixes it.
  • Which comparison would you standardise on for items read from reactive arrays, and why?
    A stable key, such as `x.id === item.id`: it is independent of which side is a proxy and survives items being replaced by fresh objects from the server. `toRaw` on both sides is the fallback when you truly need object identity.

saying these in an interview costs you the question

  • includes() can never find a raw object inside a reactive array
  • find(x => x === rawItem) works because find is instrumented
  • list[0] returns the raw object that was pushed
  • A ref holding an object returns that same object from .value
  • Wrapping every item in reactive() before building the array fixes identity