Skip to content
← All questions
Advanced

How do reactive Maps and Sets work in Vue 3?

Reactivity

Vue 3's reactive() supports Map, Set, WeakMap, and WeakSet out of the box. The Proxy intercepts collection methods like get, set, add, delete, has, and forEach, tracking reads and triggering updates on writes. You use the standard JavaScript API, and Vue handles reactivity transparently. You can use either reactive() or ref() — both work. With reactive() you interact with the collection directly; with ref() you access it through .value.

Basic usage

vue
<script setup>
const tags = reactive(new Set<string>())
const scores = reactive(new Map<string, number>())

function addTag(tag: string) {
  tags.add(tag)
}

function setScore(name: string, score: number) {
  scores.set(name, score)
}
</script>

<template>
  <div>
    <button @click="addTag('vue')">Add tag</button>
    <span v-for="tag in tags" :key="tag">{{ tag }}</span>
  </div>

  <div>
    <button @click="setScore('Alice', 95)">Set score</button>
    <div v-for="[name, score] in scores" :key="name">
      {{ name }}: {{ score }}
    </div>
  </div>
</template>

v-for works directly on Map and Set because Vue iterates them just like arrays. For a Map, each entry destructures as [key, value].

Which methods are tracked

Vue intercepts these operations:

OperationTracked (read)Triggers update (write)
map.get(key)YesNo
map.set(key, value)NoYes
map.has(key)YesNo
map.delete(key)NoYes
map.sizeYesNo
map.forEach(fn)Yes (all entries)No
set.add(value)NoYes
set.has(value)YesNo
set.delete(value)NoYes
map.clear()NoYes
Iterating (for...of, spread)Yes (all entries)No

This means computed properties and watchers that read from a reactive Map or Set will re-run when the collection is modified.

reactive() vs ref() with collections

Both work. reactive() proxies the Map/Set directly, so you call methods without .value. ref() wraps it — you access the collection through .value, and Vue makes the inner value reactive automatically.

ts
// reactive(): interact with the Map directly
const map = reactive(new Map())
map.set('key', 'val') // reactive ✅

// ref(): access through .value
const map = ref(new Map())
map.value.set('key', 'val') // also reactive ✅

reactive() is more ergonomic when you only mutate entries. ref() is better when you might need to replace the entire collection (like swapping it for fresh data from an API):

ts
const scores = shallowRef(new Map<string, number>())

async function refresh() {
  const data = await $fetch('/api/scores')
  const newMap = new Map(data.map(d => [d.name, d.score]))
  scores.value = newMap // triggers update
}

Computed properties over Maps

vue
<script setup>
const permissions = reactive(new Map<string, boolean>([
  ['read', true],
  ['write', false],
  ['admin', false]
]))

const activePermissions = computed(() =>
  [...permissions.entries()]
    .filter(([, enabled]) => enabled)
    .map(([name]) => name)
)
</script>

<template>
  <p>Active: {{ activePermissions.join(', ') }}</p>
  <button @click="permissions.set('write', true)">Grant write</button>
</template>

The computed re-evaluates when any entry in the Map changes because spreading the Map calls its iterator, which Vue tracks.

When to use Map/Set over plain objects and arrays

Use a Map whenUse a plain object when
Keys are not strings (objects, numbers, symbols)Keys are string-only
You need insertion-order iteration guaranteedOrder doesn't matter
You add/remove keys frequently (Maps are optimized for this)The shape is static
You need .size without Object.keys().lengthPerformance isn't a concern
Use a Set whenUse an array when
You need uniqueness enforced automaticallyDuplicates are valid
You check membership often (has() is O(1))You search by index
You need union/intersection/difference operationsYou need map/filter/reduce

Limitations

  1. No deep reactivity for values: if you store a plain object as a Map value, that object is NOT automatically made reactive. You'd need to wrap it with reactive() yourself before storing it.

  2. WeakMap/WeakSet are limited: they work with reactive() but you can't iterate them or check .size, which limits their usefulness in templates. They're mainly useful for internal bookkeeping in composables.

  3. Watching specific keys: watch on a reactive Map watches the entire collection. To watch a specific key, use a getter:

ts
const config = reactive(new Map<string, string>())

watch(
  () => config.get('theme'),
  (newTheme) => {
    document.documentElement.className = newTheme ?? ''
  }
)

See also: Why doesn't reactive() work with primitives? · What is the reactivity proxy identity hazard?

References

Released under the MIT License.