skip to content

In Vue 3, how does a reactive Map of sessions track get(), set() and iteration, and what breaks when keys mix raw and proxy objects?

level: middleimportance: should knowfreq 30%

answer

  1. methods are swapped for instrumented ones
  2. per-key reads, whole-map iteration
  3. values come back as proxies
  4. a dev warning about duplicate keys

basics

~20 s

Vue 3 swaps a reactive Map's methods for instrumented ones: get and has track one key, iteration and size track the whole map, and set, delete and clear trigger. Keys are matched raw-or-proxy, but storing both forms of one object creates two entries.

solid answer

~50 s

A `Map` wrapped by `reactive()` (or held in a `ref`) returns instrumented versions of its methods. `get(key)` and `has(key)` track that key; `size`, `forEach`, `keys()`, `values()` and `entries()` track iteration. Adding a key triggers that key's readers and all iteration; replacing a value triggers that key's readers, `size` and value iteration, but not a `keys()` loop; `delete` and `clear` trigger both. Values come back as reactive proxies, so mutating `sessions.get(id).lastSeen` is tracked. Object-key lookups try the key and then its raw form, and new keys are stored raw, so raw and proxied keys usually hit one entry. If both forms of one object end up stored as keys — say, proxy keys added before the map became reactive — you get two entries and a development warning about raw and reactive versions of the same object.

code

ts · 14 lines
ts
import { reactive, computed } from 'vue'

interface Session { user: string; lastSeen: number }

const sessions = reactive(new Map<string, Session>())

const online = computed(() => sessions.size)          // tracks iteration
const ada = computed(() => sessions.get('s1')?.lastSeen) // tracks key 's1'

sessions.set('s1', { user: 'ada', lastSeen: 1 })  // new key: both recompute
sessions.set('s2', { user: 'lin', lastSeen: 2 })  // online recomputes, ada does not

sessions.get('s1')!.lastSeen = 3  // value is a reactive proxy: ada recomputes
sessions.delete('s2')             // online recomputes

go deeper

for a junior

Know that Vue 3 can make Map and Set reactive and that values you get out of a reactive Map are reactive too.

for a middle

Explain the instrumented methods: per-key tracking for get and has, iteration tracking for size and loops, and which writes trigger which readers.

for a senior

Recognise the mixed raw and reactive key warning, trace how both forms got into the map, and move object-keyed maps to primitive keys.

for a principal

Set guidance for collection-shaped state: primitive keys, one identity discipline, and shallow variants where large maps are replaced wholesale.

## Why collections need special handling A plain object's properties can be intercepted with ordinary proxy handlers, but a `Map` or `Set` stores its data internally and is read through methods — `get`, `set`, `has`, `forEach`, iterators. Calling those methods on a bare proxy would not work, and even if it did, a method call is not a property read Vue could track. So for `Map`, `Set`, `WeakMap` and `WeakSet`, `reactive()` uses **collection handlers**: when you access `sessions.get`, the proxy hands back Vue's instrumented `get`, which tracks and then calls the real method on the raw map. ## What each operation tracks and triggers Take `const sessions = reactive(new Map<string, Session>())`. | Operation | Effect | |---|---| | `sessions.get(id)`, `sessions.has(id)` | tracks that key only | | `sessions.size`, `forEach`, `values()`, `entries()` | tracks the whole map's iteration | | `sessions.keys()` | tracks the set of keys | | `sessions.set(newId, s)` | triggers readers of that key, iteration and key iteration | | `sessions.set(existingId, s2)` | triggers readers of that key, `size`/value iteration — not a `keys()` loop — and only if the value changed | | `sessions.delete(id)`, `sessions.clear()` | triggers readers of the affected keys and all iteration | A component that shows one session through `get(id)` therefore re-renders when **that** session is replaced or removed, not when another user logs in; a sidebar that iterates the map re-renders on any addition or removal. ## Values are deep, keys are unwrapped - **Values** read through `get`, `forEach` or an iterator come back as reactive proxies. `sessions.get(id)!.lastSeen = Date.now()` is a tracked write on the session object itself, so anything that rendered `lastSeen` updates. - **Stored values** are unwrapped: `set(key, reactiveSession)` stores the raw object, just as reactive arrays and objects do. - **Lookups** try the key as given and then its raw form, so `get(proxyKey)` finds an entry stored under the raw object. - **New keys** are stored in raw form when the given key is not already present. ## The mixed-key trap String keys such as session ids have no identity problem. Object keys do. Keying sessions by a `user` object works as long as every entry for a user uses the same underlying object — Vue's raw fallback makes raw and proxied references land on the same entry. It breaks when both forms end up stored as **separate keys**: 1. entries are added to a plain `Map` using proxies (for example `user` objects read from reactive state); 2. the map is later made reactive, or passed through `ref()`; 3. new code sets the same user using the raw object. Now the map has two keys for one user. In development, a later `set` or `delete` that uses the proxy key finds the raw form present as well and warns: 'Reactive Map contains both the raw and reactive versions of the same object as keys, which can lead to inconsistencies.' The advice in that warning is to avoid differentiating between the two and use the reactive version. ## Practical guidance - Prefer **primitive keys** (ids) for reactive Maps; they sidestep identity entirely. - If you must key by object, keep one discipline — all proxies or all raw — from the moment the map is created. - A `WeakMap` or `WeakSet` is instrumented the same way but has no iteration, so only per-key reads are tracked. - For very large maps whose values are replaced wholesale, the shallow variants are the tool; choosing them is a performance decision rather than an identity one.

  • In Vue 3, does `reactive(new Set()).has(p)` find an object that was added in raw form, when p is that object's reactive proxy?
    Yes. `has` checks the value as given and then its raw form, and `add` stores raw objects, so the proxy lookup falls back to the raw entry. Adding the proxy afterwards is deduplicated as well: `add` unwraps it, sees the raw object is already present, and does nothing.
  • Why does replacing an existing session with set() not re-run a loop over sessions.keys()?
    The set of keys did not change. Vue triggers value iteration and `size` readers for a Map `SET`, but it tracks `keys()` separately and only triggers it when a key is added or removed, so key-only loops are spared.

saying these in an interview costs you the question

  • Map and Set cannot be made reactive in Vue 3, only objects and arrays
  • Any Map change re-runs every effect that touched the map
  • Values returned by get() are raw, so nested mutations are not tracked
  • A raw key and its proxy always create two separate entries
  • WeakMap iteration is tracked like a Map's