mit Map und Set praktisch nutzen: Auswahl-Sets und Lookup-Maps
Die meisten Vue-Tutorials arbeiten fast ausschließlich mit Arrays und einfachen Objects, dabei bringt JavaScript mit Map und Set zwei Collection-Typen mit, die für bestimmte Aufgaben deutlich besser passen. Vues Reactivity-System unterstützt beide vollständig, sodass reactive(new Map()) und reactive(new Set()) genauso zuverlässig auf Änderungen reagieren wie ein reaktives Array. Wer weiß, wann eine Set-basierte Auswahl in einer Multi-Select-UI oder eine Map-basierte Lookup-Struktur die bessere Wahl ist, und welche Fallstricke bei der Destrukturierung lauern, schreibt spürbar robusteren und schnelleren Code.
Inhaltsverzeichnis
- 1. Warum Map und Set neben Arrays in Vue-Projekten relevant sind
- 2. reactive(new Map()) im Detail: warum es funktioniert
- 3. Vorsicht bei Destrukturierung: was die Reaktivität bricht
- 4. Praxisbeispiel: eindeutige Auswahl-Sets in einer Multi-Select-UI
- 5. Praxisbeispiel: Lookup-Maps für schnellen Zugriff auf Detaildaten
- 6. Performance-Vergleich: Map-Lookup gegen Array.find()
- 7. ref() versus reactive() für Map und Set: was ist sinnvoller
- 8. Watchen von Map- und Set-Änderungen: die deep-Option und ihre Grenzen
- 9. Häufige Fehler und Fazit
- 10. Zusammenfassung
- 11. FAQ
1. Warum Map und Set neben Arrays in Vue-Projekten relevant sind
Ein Array ist die naheliegende Wahl für eine Liste von Elementen, verliert aber schnell an Komfort, sobald zwei Anforderungen gleichzeitig gefragt sind: eindeutige Werte ohne manuelle Duplikatprüfung, und schneller Zugriff per Schlüssel statt per Index oder linearer Suche. Ein Set garantiert von Haus aus, dass jeder Wert nur einmal enthalten sein kann, was es zur natürlichen Datenstruktur für Auswahlzustände macht, bei denen jedes Element entweder ausgewählt oder nicht ausgewählt ist. Eine Map wiederum bildet Schlüssel auf Werte ab und erlaubt dabei beliebige Schlüsseltypen, nicht nur Strings wie bei einem klassischen Object.
In der Praxis begegnen einem beide Strukturen regelmäßig: ein Set für die IDs ausgewählter Zeilen in einer Tabelle, eine Map für ein Lookup von Produkt-ID auf vollständiges Produktobjekt, um wiederholte Array.find()-Aufrufe zu vermeiden. Vue behandelt beide Typen als vollwertige Bürger im Reactivity-System, das bedeutet, dass set(), add(), delete() und clear() alle registrierten Effekte auf dieselbe Weise auffrischen wie push() oder splice() bei einem reaktiven Array.
2. reactive(new Map()) im Detail: warum es funktioniert
Vues reactive() erzeugt einen Proxy um das übergebene Objekt, und dieser Proxy überwacht bei Map und Set nicht nur den lesenden Zugriff auf einzelne Einträge, sondern auch die mutierenden Methoden der jeweiligen Collection selbst. Ruft man auf einer reaktiven Map map.set(key, value) auf, erkennt Vue intern, dass sich der Zustand der Collection geändert hat, und benachrichtigt alle Effekte, die zuvor lesend auf diese Map zugegriffen haben, etwa über map.get(key), map.has(key) oder map.size.
Wichtig dabei ist, dass die Reaktivität an der reactive()-Proxy-Instanz selbst hängt, nicht an der zugrunde liegenden, ursprünglichen Map. Wird die Map in einem ref() gehalten statt direkt mit reactive() gewrappt, muss innerhalb von script setup weiterhin .value verwendet werden, um an die eigentliche Map-Instanz zu gelangen, bevor set(), get() oder size angesprochen werden können. Im Template dagegen entfällt .value automatisch durch das eingebaute Auto-Unwrapping von Refs auf oberster Ebene.
import { reactive } from 'vue'
interface Product {
id: string
name: string
price: number
}
const productLookup = reactive(new Map<string, Product>())
function addProduct(product: Product) {
productLookup.set(product.id, product)
}
function removeProduct(id: string) {
productLookup.delete(id)
}
// productLookup.size und productLookup.get(id) sind vollständig reaktiv
3. Vorsicht bei Destrukturierung: was die Reaktivität bricht
Der häufigste Stolperstein bei reaktiven Map- und Set-Instanzen ist die Destrukturierung, weil Methoden wie map.get oder map.has als eigenständige Funktionsreferenzen aus dem Proxy herausgelöst ihren internen Bezug auf this verlieren. Ein Aufruf wie const { get, set } = productLookup gefolgt von get('abc') führt zur Laufzeit zu einem TypeError, weil get intern erwartet, über die Proxy-Instanz aufgerufen zu werden, um korrekt auf die zugrunde liegende Map zuzugreifen.
Der sichere Weg ist, die Map- oder Set-Instanz als Ganzes zu referenzieren und Methoden stets direkt darauf aufzurufen, also productLookup.get(id) statt einer vorab destrukturierten get-Funktion. Wer eine einzelne, reaktive Ableitung braucht, etwa nur die aktuelle Größe, sollte dafür ein computed() verwenden, das intern weiterhin productLookup.size liest, statt size einmalig zu destrukturieren und damit auf einen statischen Schnappschuss einzufrieren.
// Falsch: bricht die Reaktivität und den this-Bezug
const { get, has } = productLookup
// get('abc') wirft zur Laufzeit einen TypeError
// Richtig: Methoden direkt auf der Instanz aufrufen
productLookup.get('abc')
productLookup.has('abc')
// Richtig für eine abgeleitete, reaktive Größe
import { computed } from 'vue'
const productCount = computed(() => productLookup.size)
4. Praxisbeispiel: eindeutige Auswahl-Sets in einer Multi-Select-UI
In einer Tabelle mit Mehrfachauswahl, etwa für Bulk-Aktionen auf mehreren Zeilen, ist ein Set die passendste Datenstruktur für die ausgewählten IDs, weil Doppelauswahl strukturell ausgeschlossen ist und Zugehörigkeitsprüfungen per has() in konstanter Zeit erfolgen. Ein Array würde entweder manuelle Duplikatprüfung vor jedem push() erfordern, oder bei jeder Sichtbarkeitsprüfung eine lineare includes()-Suche durch die gesamte Liste, was bei großen Tabellen spürbar langsamer wird.
Die Toggle-Logik für eine einzelne Zeile reduziert sich mit einem Set auf drei Zeilen: prüfen, ob die ID bereits enthalten ist, und je nach Ergebnis add() oder delete() aufrufen. Für Select-All-Funktionalität lässt sich das Set einfach per clear() leeren oder mit allen sichtbaren IDs neu befüllen, wobei sich reactive(new Set()) in dieser Konstellation besonders anbietet, weil das Template dann direkt selectedIds.has(row.id) für jede Zeile abfragen kann, ohne zusätzliche computed-Werte anzulegen.
import { reactive } from 'vue'
const selectedIds = reactive(new Set<string>())
function toggleSelection(id: string) {
if (selectedIds.has(id)) {
selectedIds.delete(id)
} else {
selectedIds.add(id)
}
}
function selectAll(ids: string[]) {
ids.forEach((id) => selectedIds.add(id))
}
function clearSelection() {
selectedIds.clear()
}
5. Praxisbeispiel: Lookup-Maps für schnellen Zugriff auf Detaildaten
Sobald eine Komponente wiederholt anhand einer ID auf ein Objekt in einer größeren Liste zugreifen muss, etwa um in einer Warenkorb-Ansicht zu jeder Position die vollständigen Produktdaten nachzuschlagen, lohnt sich eine begleitende Lookup-Map neben dem eigentlichen Array. Statt bei jedem Zugriff products.find(p => p.id === id) durch das gesamte Array zu iterieren, liefert productMap.get(id) das Ergebnis in konstanter Zeit, unabhängig von der Größe der ursprünglichen Liste.
Diese Lookup-Map wird typischerweise als computed() aus der ursprünglichen Datenquelle abgeleitet, etwa aus einem per API geladenen Array, damit sie automatisch neu aufgebaut wird, sobald sich die Datenquelle ändert, ohne dass zwei Kopien der Daten manuell synchron gehalten werden müssen. Für häufig aktualisierte, große Listen mit mehreren hundert oder tausend Einträgen macht dieser Unterschied in der Praxis spürbar aus, ob eine Detailansicht sofort reagiert oder mit jedem zusätzlichen Eintrag merklich träger wird.
6. Performance-Vergleich: Map-Lookup gegen Array.find()
Algorithmisch betrachtet liegt der Unterschied zwischen einer Map und einem Array bei der Suche in der Komplexität: Array.find() durchsucht im schlechtesten Fall jedes Element linear, also mit einer Komplexität von O(n), während map.get() dank Hashing im Durchschnitt in konstanter Zeit, also O(1), arbeitet, unabhängig von der Anzahl der Einträge. Bei kleinen Listen mit wenigen Dutzend Einträgen ist dieser Unterschied in der Praxis kaum messbar, weil moderne JavaScript-Engines auch kleine lineare Suchen extrem schnell abarbeiten.
Relevant wird der Unterschied erst bei größeren Datenmengen oder bei Zugriffsmustern, die denselben Lookup sehr häufig innerhalb kurzer Zeit wiederholen, etwa beim Rendern einer langen Tabelle, in der für jede Zeile ein Lookup in einer verwandten Liste stattfindet. In diesem Szenario summieren sich viele einzelne O(n)-Suchen schnell zu einer insgesamt quadratischen Laufzeit, während dieselbe Aufgabe mit einer vorab aufgebauten Map linear bleibt, was den einmaligen Aufwand für den Aufbau der Map in aller Regel mehr als aufwiegt.
7. ref() versus reactive() für Map und Set: was ist sinnvoller
Sowohl ref(new Map()) als auch reactive(new Map()) machen eine Map reaktiv, unterscheiden sich aber im praktischen Umgang. Bei reactive() wird direkt auf der zurückgegebenen Instanz gearbeitet, also productLookup.set(id, value) ohne .value, was sich im Skript-Code intuitiver liest und der üblichen Verwendung von reactive() für objektartige Strukturen entspricht. Bei ref() muss dagegen konsequent .value vorangestellt werden, etwa productLookup.value.set(id, value), was zwar mehr Tipparbeit bedeutet, aber den Vorteil hat, dass sich die gesamte Map jederzeit unkompliziert durch eine komplett neue Instanz ersetzen lässt, etwa productLookup.value = new Map(freshEntries).
In der Praxis ist reactive() für Map und Set meist die angenehmere Wahl, solange die Collection ausschließlich über ihre eigenen Methoden verändert wird und niemals als Ganzes neu zugewiesen werden muss. Sobald ein Anwendungsfall regelmäßig die komplette Collection austauscht, etwa beim Neuladen aller Daten nach einem API-Call, ist ref() die robustere Wahl, weil eine mit reactive() gewrappte Variable nicht neu zugewiesen werden kann, ohne die ursprüngliche Reaktivitätsverbindung zu verlieren.
8. Watchen von Map- und Set-Änderungen: die deep-Option und ihre Grenzen
Ein watch() auf eine reaktive Map oder ein reaktives Set reagiert standardmäßig nur auf eine komplette Neuzuweisung der Referenz, nicht auf einzelne set()-, add()- oder delete()-Aufrufe, weil sich die Referenz auf das Proxy-Objekt selbst dabei nicht ändert. Um auf interne Mutationen zu reagieren, muss watch() mit der Option { deep: true } aufgerufen werden, wodurch Vue interne Änderungen an der Collection ebenfalls erkennt und den Callback entsprechend auslöst, auch ohne dass sich die äußere Referenz ändert.
Bei sehr großen Maps oder Sets kann { deep: true } spürbaren Overhead verursachen, weil Vue bei jeder Mutation die interne Struktur überprüfen muss. Für performancekritische Fälle ist es oft sinnvoller, gezielt einzelne, davon abgeleitete computed-Werte zu beobachten, etwa computed(() => productLookup.size), statt die gesamte Collection tief zu watchen, weil sich ein computed automatisch und effizient nur dann neu berechnet, wenn tatsächlich gelesene Abhängigkeiten sich geändert haben.
9. Häufige Fehler und Fazit
Der häufigste Fehler ist die eingangs beschriebene Destrukturierung von Methoden, die den this-Bezug bricht und zur Laufzeit zu einem TypeError führt, obwohl der Code auf den ersten Blick unauffällig aussieht. Ein zweiter häufiger Fehler ist, ein watch() ohne { deep: true } auf eine Map oder ein Set zu setzen und sich zu wundern, warum interne Änderungen keine Reaktion auslösen, während eine komplette Neuzuweisung der Collection problemlos funktioniert.
Als Fazit gilt: Map und Set sind kein exotisches Nischenwerkzeug, sondern in Vue 3 vollwertig unterstützte Reactivity-Primitive, die für Auswahlzustände und Lookup-Strukturen oft die klarere und schnellere Wahl gegenüber Arrays sind. Wer Methoden stets direkt auf der Instanz aufruft, reactive() für inkrementell veränderte Collections und ref() für regelmäßig komplett ersetzte Collections nutzt, und bei Bedarf gezielt mit { deep: true } oder computed() arbeitet, holt aus beiden Strukturen das volle Potenzial heraus.
| Struktur | Typischer Einsatz | Zugriffszeit | Reaktivität in Vue |
|---|---|---|---|
| Array | Geordnete Liste, Iteration, Rendering | O(n) für Suche per find() | Voll unterstützt via reactive() |
| Set | Eindeutige Auswahl, Zugehörigkeitsprüfung | O(1) für has() | Voll unterstützt via reactive() |
| Map | Schlüssel-Wert-Lookup, Detaildaten nach ID | O(1) für get() | Voll unterstützt via reactive() |
| Object | Einfache Schlüssel-Wert-Paare mit String-Keys | O(1) für direkten Property-Zugriff | Voll unterstützt via reactive() |
Mironsoft
Vue-Architektur, Composition API und Nuxt-Performance
Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?
Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.
Architektur-Review
Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.
Performance-Audit
Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.
Nuxt-Integration
SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.
10. Zusammenfassung
Reactive Map und Set in Vue: Das Wichtigste auf einen Blick
Set für Auswahl
Garantiert eindeutige Werte und liefert has()-Prüfungen in konstanter Zeit, ideal für Multi-Select-Zustände.
Map für Lookup
Bildet Schlüssel auf Werte ab und ersetzt lineares Array.find() durch konstante get()-Zugriffe.
Destrukturierungsfalle
Methoden nie destrukturieren, immer direkt auf der reaktiven Instanz aufrufen, sonst bricht der this-Bezug.
watch mit deep
Interne Mutationen wie set() oder add() erfordern watch() mit der Option { deep: true }.