Skip to content
← Todas las preguntas
Avanzado

¿Cómo funcionan los Maps y Sets reactivos en Vue 3?

Reactividad

reactive() de Vue 3 soporta Map, Set, WeakMap y WeakSet de serie. El Proxy intercepta los métodos de colección como get, set, add, delete, has y forEach, registrando las lecturas y disparando actualizaciones en las escrituras. Se usa la API estándar de JavaScript y Vue gestiona la reactividad de forma transparente. Puedes usar tanto reactive() como ref() — ambos funcionan. Con reactive() interactúas con la colección directamente; con ref() accedes a ella a través de .value, y Vue hace el valor interno reactivo automáticamente.

Uso básico

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 funciona directamente con Map y Set porque Vue los itera igual que arrays. En un Map, cada entrada se desestructura como [key, value].

Qué métodos se registran

Vue intercepta estas operaciones:

OperaciónRegistrada (lectura)Dispara actualización (escritura)
map.get(key)No
map.set(key, value)No
map.has(key)No
map.delete(key)No
map.sizeNo
map.forEach(fn)Sí (todas las entradas)No
set.add(value)No
set.has(value)No
set.delete(value)No
map.clear()No
Iteración (for...of, spread)Sí (todas las entradas)No

Esto significa que las propiedades computed y los watchers que leen de un Map o Set reactivo se volverán a ejecutar cuando se modifique la colección.

reactive() vs ref() con colecciones

Ambos funcionan. reactive() hace proxy del Map/Set directamente, así que llamas a los métodos sin .value. ref() lo envuelve — accedes a la colección a través de .value, y Vue hace el valor interno reactivo automáticamente.

ts
// reactive(): interactúa con el Map directamente
const map = reactive(new Map())
map.set('key', 'val') // reactivo ✅

// ref(): accede a través de .value
const map = ref(new Map())
map.value.set('key', 'val') // también reactivo ✅

reactive() es más ergonómico cuando solo mutas entradas. ref() es mejor cuando puede que necesites reemplazar toda la colección (como cuando la intercambias por datos frescos de una 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 // dispara la actualización
}

Propiedades computed sobre 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>

El computed se re-evalúa cuando cambia cualquier entrada del Map porque al hacer spread del Map se llama a su iterador, que Vue registra.

Cuándo usar Map/Set frente a objetos planos y arrays

Usar un Map cuandoUsar un objeto plano cuando
Las claves no son strings (objetos, números, símbolos)Las claves son solo strings
Se necesita iteración garantizada en orden de inserciónEl orden no importa
Se añaden/eliminan claves frecuentemente (los Maps están optimizados para esto)La forma es estática
Se necesita .size sin Object.keys().lengthEl rendimiento no es una preocupación
Usar un Set cuandoUsar un array cuando
Se necesita unicidad garantizada automáticamenteLos duplicados son válidos
Se comprueba la pertenencia a menudo (has() es O(1))Se busca por índice
Se necesitan operaciones de unión/intersección/diferenciaSe necesita map/filter/reduce

Limitaciones

  1. Sin reactividad profunda para los valores: si se almacena un objeto plano como valor de un Map, ese objeto NO se convierte automáticamente en reactivo. Habría que envolverlo con reactive() antes de almacenarlo.

  2. WeakMap/WeakSet son limitados: funcionan con reactive() pero no se pueden iterar ni comprobar .size, lo que limita su utilidad en templates. Son útiles principalmente para bookkeeping interno en composables.

  3. Observar claves específicas: watch sobre un Map reactivo observa toda la colección. Para observar una clave específica, usar un getter:

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

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

Ver también: ¿Por qué reactive() no funciona con primitivos? · ¿Qué es el problema de identidad del proxy en reactividad?

Referencias

Publicado bajo la licencia MIT.