<v/>
{ }
Vue 3 · Composition API · TypeScript · Pinia
50 Vue 3 Patterns für produktive Frontends
von Composables bis zu performanten Reaktivitätsmustern

Wer Vue 3 mit denselben Patterns wie Vue 2 einsetzt, verschenkt das Potenzial der Composition API. Composables, defineModel, Suspense und reaktive Stores mit Pinia ersetzen fragile Options-API-Konstrukte durch wartbare, testbare und typsichere Vue 3 Patterns, die auch in großen Teams skalieren.

20 Min. Lesezeit Composition API · Composables · defineModel · Pinia · Suspense Vue 3.4+ · Vite · TypeScript

1. Was Vue 3 Patterns wirklich lösen

Ein Vue 3 Pattern ist keine Stilentscheidung, sondern eine erprobte Lösungsstruktur für ein wiederkehrendes Problem in der Frontend-Entwicklung. Der Unterschied zu einer ad-hoc-Lösung liegt darin, dass das Muster gezielt auf Wiederverwendbarkeit, Testbarkeit und Typsicherheit ausgelegt ist. Vue 3 brachte mit der Composition API ein völlig neues Paradigma, das diese Patterns überhaupt erst ermöglicht – aber nur dann entfaltet es sein Potenzial, wenn man es konsequent anwendet statt alte Options-API-Denkmuster einfach zu übersetzen.

In der Praxis sieht man häufig, wie Teams Vue 3 einsetzen, aber weiterhin mit data(), computed: und methods: in der Options-API arbeiten, weil sie vertraut ist. Das ist technisch korrekt, aber verschenkt alles, was Vue 3 an Kompositionsmöglichkeiten mitbringt. Ein Vue 3 Pattern wie das Composable löst das Grundproblem der Options-API: Logik, die in data, computed, methods und lifecycle hooks verstreut ist, wird untrennbar an die Komponente gebunden und kann nicht ohne Weiteres wiederverwendet werden. Die folgenden Abschnitte zeigen die wichtigsten Vue 3 Patterns – vom Setup-Fundament über reaktive Stores bis hin zu Performance-Optimierungen.

2. Composition API: setup(), ref und reactive richtig einsetzen

Das Fundament aller modernen Vue 3 Patterns ist die Composition API mit ihrer setup()-Funktion, die seit Vue 3.2 durch das <script setup>-Syntaxzucker noch einfacher genutzt werden kann. Der erste wichtige Entscheid: ref versus reactive. ref wrappet jeden Wert – primitive wie Objekte – in einen reaktiven Container, der über .value zugegriffen wird. reactive macht ein Objekt direkt reaktiv, ohne .value, verliert aber die Reaktivität wenn man Eigenschaften destrukturiert. Das Vue 3 Pattern für die meisten Situationen: ref für skalare Werte und Zustand, der ausgetauscht werden soll, reactive für eng zusammengehörende Objektstrukturen wie Formulardaten.

Das <script setup>-Pattern ist in Vue 3.2+ die empfohlene Schreibweise. Alles, was im Top-Level deklariert wird, ist automatisch im Template verfügbar – kein explizites return erforderlich. defineProps und defineEmits sind Compiler-Makros, die typsichere Props und Events ohne Imports ermöglichen. Das Muster withDefaults(defineProps<Props>(), {...}) kombiniert TypeScript-Typsicherheit mit Standardwerten in einer einzigen, lesbaren Deklaration. Ein häufiger Fehler: reaktive State außerhalb von setup() oder <script setup> deklarieren – so verlieren Refs ihre Reaktivitätsverbindung zur Komponenteninstanz.


<!-- ProductCard.vue — Composition API with script setup -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Product } from '@/types'

// Props with TypeScript types and defaults
interface Props {
  product: Product
  currency?: string
  showStock?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  currency: 'EUR',
  showStock: true,
})

// Emits — typed for autocomplete and runtime validation
const emit = defineEmits<{
  addToCart: [productId: number, quantity: number]
  wishlist: [productId: number]
}>()

// Local state — ref for primitives, reactive for form groups
const quantity = ref(1)
const isLoading = ref(false)

// Derived state — computed caches automatically
const formattedPrice = computed(() =>
  new Intl.NumberFormat('de-DE', { style: 'currency', currency: props.currency })
    .format(props.product.price)
)

const canAddToCart = computed(() =>
  !isLoading.value && props.product.stock > 0 && quantity.value >= 1
)

// Lifecycle
onMounted(() => {
  console.log('ProductCard mounted for:', props.product.id)
})

function handleAddToCart() {
  if (!canAddToCart.value) return
  emit('addToCart', props.product.id, quantity.value)
}
</script>

Ein weiteres wichtiges Vue 3 Pattern ist die konsequente Trennung von reaktivem Zustand und nicht-reaktiven Konstanten. Werte, die sich nie ändern, sollten mit const außerhalb der reaktiven Schicht deklariert werden – das reduziert den Overhead des Reaktivitätssystems. shallowRef und shallowReactive sind spezialisierte Varianten für große Datenstrukturen, bei denen nur die oberste Ebene reaktiv sein muss. Ein shallowRef mit einem großen Array triggert Reaktivität nur, wenn das Array selbst ausgetauscht wird – nicht bei Mutationen der Elemente. Das ist ein entscheidendes Vue 3 Pattern für Listen mit vielen Einträgen.

3. Composables: Logik extrahieren und wiederverwenden

Das wichtigste architekturelle Vue 3 Pattern ist das Composable. Ein Composable ist eine Funktion, die use als Präfix trägt, die Composition API intern nutzt und reaktiven Zustand sowie Methoden zurückgibt. Das macht Logik, die früher an eine Komponente gebunden war, vollständig wiederverwendbar – ohne Mixins, ohne Vererbung und ohne die versteckten Kollisionen von Mixin-Namespacing. Ein useFetch()-Composable kapselt Lade-Zustand, Fehlerbehandlung und das eigentliche Ergebnis in einer Funktion, die in jeder Komponente aufgerufen werden kann.

Das Vue 3 Pattern für saubere Composables: Sie sollten in sich geschlossen sein und nur das zurückgeben, was der Aufrufer braucht. Reaktiver State innerhalb eines Composables ist privat, solange er nicht explizit zurückgegeben wird. Das ermöglicht, denselben Composable mehrfach in derselben Komponente aufzurufen – jede Instanz bekommt ihren eigenen isolierten State. Im Gegensatz dazu teilen Pinia-Stores ihren State zwischen allen Konsumenten – das ist ein grundlegender Unterschied, der die Wahl zwischen Composable und Store bestimmt: lokaler Komponentenzustand gehört ins Composable, globaler Applikationszustand in den Store.


// composables/useFetch.ts — Reusable data-fetching composable
import { ref, watch, type Ref } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  isLoading: Ref<boolean>
  execute: () => Promise<void>
}

export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  // Abort controller to cancel in-flight requests on URL change
  let controller: AbortController | null = null

  async function execute() {
    const resolvedUrl = typeof url === 'string' ? url : url.value
    controller?.abort()
    controller = new AbortController()

    isLoading.value = true
    error.value = null

    try {
      const res = await fetch(resolvedUrl, { signal: controller.signal })
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
      data.value = await res.json() as T
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        error.value = err as Error
      }
    } finally {
      isLoading.value = false
    }
  }

  // Re-fetch automatically when URL changes
  if (typeof url !== 'string') {
    watch(url, execute, { immediate: true })
  } else {
    execute()
  }

  return { data, error, isLoading, execute }
}

4. defineModel und Props-Patterns für saubere Komponenten-APIs

Vue 3.4 führte defineModel() als stabiles Compiler-Makro ein – eines der wichtigsten neuen Vue 3 Patterns für Formular-Komponenten. Vorher erforderte eine v-model-kompatible Komponente die explizite Deklaration eines modelValue-Props und eines update:modelValue-Emits sowie die manuelle Verkettung in computed getters und setters. defineModel() kapselt genau dieses Muster in einer einzigen Zeile und gibt ein reaktives Ref zurück, das direkt gelesen und beschrieben werden kann – Vue kümmert sich um den Props-Down-Events-Up-Zyklus im Hintergrund.

Für komplexere Komponenten-APIs ist das Vue 3 Pattern mit mehreren benannten Models besonders wertvoll. Ein Modal-Komponente kann gleichzeitig v-model:visible und v-model:activeTab exponieren – jedes mit eigenem Typ und eigenem Standardwert. Das Props-Typing-Pattern mit defineProps<Props>() kombiniert TypeScript-Interfaces mit Vue's Laufzeit-Validierung. Für Props, die entweder ein primitiver Wert oder undefined sein können, nutzt man PropType aus Vue's Typdefinitionen, um komplexe Typen wie Union-Types und generische Interfaces korrekt abzubilden.

5. Pinia: reaktive Stores ohne Boilerplate

Pinia ist der offizielle State-Manager für Vue 3 und verkörpert das moderne Vue 3 Pattern für globalen Applikationszustand. Im Gegensatz zu Vuex 4 gibt es keine Mutations, keine Namespaced-Module-Verschachtelung und kein explizites Commit-Boilerplate. Ein Pinia-Store ist konzeptionell nichts anderes als ein Composable mit persistiertem, geteiltem Zustand: state wird mit ref oder reactive definiert, getters mit computed und actions mit normalen Funktionen. Das Vue 3 Pattern für Pinia bevorzugt die Setup-Store-Variante gegenüber der Options-Store-Variante, weil sie denselben mentalen Rahmen wie <script setup> verwendet und vollständige TypeScript-Inferenz bietet.

Ein wichtiges Vue 3 Pattern für Pinia-Stores: Actions sollten die einzige Stelle sein, an der State mutiert wird. Direkte State-Mutationen aus Komponenten heraus funktionieren technisch, verletzen aber das Prinzip der nachvollziehbaren Zustandsübergänge. Mit dem Pinia-Devtools-Plugin in den Vue Devtools sieht man jeden State-Übergang mit Action-Name, Vor- und Nachzustand sowie Zeitstempel – das ist nur dann sinnvoll, wenn Mutationen über Actions laufen. Das Store-Persist-Plugin von Pinia ermöglicht, ausgewählte Store-Eigenschaften im localStorage zu speichern – ein typisches Vue 3 Pattern für User-Einstellungen wie Theme und Sprachauswahl.


// stores/cart.ts — Pinia setup store
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { CartItem, Product } from '@/types'

export const useCartStore = defineStore('cart', () => {
  // State — plain refs, fully typed
  const items = ref<CartItem[]>([])
  const couponCode = ref<string | null>(null)

  // Getters — computed, cached automatically
  const totalItems = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const subtotal = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const isEmpty = computed(() => items.value.length === 0)

  // Actions — the only place state should be mutated
  function addItem(product: Product, quantity = 1) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity += quantity
    } else {
      items.value.push({ ...product, quantity })
    }
  }

  function removeItem(productId: number) {
    items.value = items.value.filter(i => i.id !== productId)
  }

  function clearCart() {
    items.value = []
    couponCode.value = null
  }

  return { items, couponCode, totalItems, subtotal, isEmpty, addItem, removeItem, clearCart }
})

6. Performance-Patterns: computed, shallowRef und v-memo

Vue's Reaktivitätssystem ist automatisch, aber nicht magisch – falsch eingesetzte Reaktivität führt zu unnötigen Re-Renders, die sich in messbarer Trägheit der UI niederschlagen. Das grundlegende Vue 3 Pattern für Performance: alle abgeleiteten Werte in computed statt in Methoden ausdrücken. Ein computed-Wert wird nur dann neu berechnet, wenn sich seine Abhängigkeiten tatsächlich ändern und er im Template verwendet wird. Eine Methode hingegen läuft bei jedem Render-Zyklus neu durch, selbst wenn die Eingaben unverändert sind. Für große Listen ist v-memo das spezialisierte Vue 3 Pattern: es memorisiert einen Template-Teilbaum und rendert ihn nur neu, wenn sich die angegebenen Abhängigkeiten ändern.

shallowRef ist das Vue 3 Pattern für große, selten tief mutierte Datenstrukturen wie einen kompletten API-Response mit hundert Produkten. Vue verfolgt bei einem normalen ref jede verschachtelte Eigenschaft reaktiv – bei einer tiefen Struktur bedeutet das erheblichen Initialisierungsaufwand. Ein shallowRef macht nur die Referenz selbst reaktiv, nicht die Inhalte. Wenn der gesamte Array ersetzt wird, triggert die Reaktivität korrekt. Einzelne Elemente lassen sich durch triggerRef() manuell reaktiv machen, wenn es nötig ist. Das Vue 3 Pattern für Long-Lists kombiniert shallowRef mit virtualisiertem Rendering durch vue-virtual-scroller oder TanStack Virtual.

7. Suspense und async Komponenten für bessere UX

Vue 3's <Suspense>-Komponente löst ein klassisches Frontend-Problem mit einem eleganten Vue 3 Pattern: das deklarative Handling von asynchronen Komponenten-Initialisierungen. Wenn eine Komponente in ihrem setup() ein await enthält – oder einen async Composable verwendet, der ein Promise zurückgibt – kann sie von <Suspense> umschlossen werden. Solange das Promise aussteht, rendert Suspense das #fallback-Slot; sobald es aufgelöst ist, zeigt es das #default-Slot. Das ersetzt die früher übliche Pattern mit lokalen isLoading-Booleans in jeder Komponente durch einen zentralen, deklarativen Mechanismus.

Async Komponenten mit defineAsyncComponent() sind das Vue 3 Pattern für Code-Splitting auf Komponenten-Ebene. Vite und Rollup erkennen dynamische Imports automatisch und erzeugen separate Chunks für jede async Komponente. Das Ergebnis: die initiale Bundle-Größe bleibt klein, schwere Komponenten wie Rich-Text-Editoren, Chart-Libraries oder Modals werden erst geladen, wenn sie tatsächlich gerendert werden sollen. defineAsyncComponent akzeptiert ein Options-Objekt mit loadingComponent, errorComponent und delay, was das Vue 3 Pattern für professionelles Lazy-Loading komplett macht.


<!-- AsyncDashboard.vue — Suspense + defineAsyncComponent pattern -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// Heavy chart component — loaded on demand, separate bundle chunk
const SalesChart = defineAsyncComponent({
  loader: () => import('@/components/SalesChart.vue'),
  loadingComponent: () => import('@/components/Skeleton.vue'),
  errorComponent: () => import('@/components/ErrorCard.vue'),
  delay: 200,    // Show loading only after 200ms (avoid flash for fast loads)
  timeout: 8000, // Show error after 8s
})
</script>

<template>
  <!-- Suspense handles async setup() in child components -->
  <Suspense>
    <template #default>
      <div class="grid grid-cols-2 gap-6">
        <SalesChart :period="selectedPeriod" />
        <RevenueTable :limit="10" />
      </div>
    </template>
    <template #fallback>
      <div class="grid grid-cols-2 gap-6">
        <SkeletonCard class="h-64" />
        <SkeletonCard class="h-64" />
      </div>
    </template>
  </Suspense>
</template>

8. Typische Fehler und wie man sie erkennt

Der häufigste Fehler in Vue 3 ist das versehentliche Verlieren der Reaktivität durch Destrukturierung. Wenn man ein reactive-Objekt destrukturiert – const { name, price } = reactive({name: 'X', price: 10}) – sind die extrahierten Eigenschaften gewöhnliche Variablen, keine reaktiven Refs mehr. Änderungen an name oder price werden vom Template nicht wahrgenommen. Das korrekte Vue 3 Pattern: toRefs() oder toRef() verwenden, wenn man Eigenschaften eines reaktiven Objekts destrukturieren und dabei die Reaktivität behalten möchte. In Composables, die ein reactive-Objekt zurückgeben, ist toRefs() beim Return-Statement Pflicht, damit der Aufrufer sicher destrukturieren kann.

Ein zweiter häufiger Fehler ist das Mutieren von Props direkt in der Kind-Komponente. Vue's Props-Down-Events-Up-Prinzip sagt: Daten fließen nach unten durch Props, Änderungen nach oben durch Emits. Wer ein Array-Prop direkt mutiert, erzeugt eine Verletzung des Datenflusses, die zu schwer nachvollziehbaren Zustandsinkonsistenzen führt. Das richtige Vue 3 Pattern: eine lokale Kopie des Props mit ref(props.items) anlegen oder defineModel() nutzen, das den Props-Update-Zyklus korrekt implementiert. Ein dritter klassischer Fehler: Watchers mit watch statt watchEffect einsetzen, wenn man alle Abhängigkeiten automatisch tracken möchte. watch erfordert explizite Source-Deklaration; watchEffect trackt automatisch alle reaktiven Werte, die im Callback gelesen werden.

9. Vue 3 Patterns im direkten Vergleich

Viele Aufgaben in Vue lassen sich auf mehrere Arten lösen – mit erheblichen Unterschieden in Wartbarkeit und Korrektheit. Die Wahl des richtigen Vue 3 Patterns hat direkte Auswirkungen auf die Langlebigkeit der Codebasis.

Aufgabe Options API / veraltet Vue 3 Pattern Vorteil
Logik teilen Mixin mit Kollisionsgefahr Composable (useFoo) Isoliert, testbar, kein Namenskonflikt
Zwei-Wege-Binding modelValue + emit manuell defineModel() Einzeiler, typsicher, Vue-intern
Globaler State Vuex mit Mutations Pinia Setup Store Kein Boilerplate, volle TS-Inferenz
Async Laden isLoading Boolean pro Komponente Suspense + async setup Deklarativ, zentral, wiederverwendbar
Code-Splitting Alles in einem Bundle defineAsyncComponent Lazy-Load, kleinere Initial-Bundle

Die Spalte "Vue 3 Pattern" zeigt nicht nur modernere Syntax – jedes der gelisteten Patterns löst ein konkretes Wartbarkeitsproblem, das die alte Variante mitbrachte. Mixins kollidierten bei gleichnamigen Eigenschaften still. Vuex-Mutations machten Typisierung umständlich. Manuelle isLoading-Flags führten zu inkonsistenter Fehlerbehandlung zwischen Komponenten. Wer die Vue 3 Patterns konsequent einsetzt, bekommt automatisch testbareren, typsichereren und wartbareren Code.

Mironsoft

Vue 3 Frontend-Entwicklung, Architektur und Performance

Vue 3 Frontend, das in großen Teams skaliert?

Wir analysieren bestehende Vue-Applikationen, identifizieren fragile Muster und migrieren sie auf bewährte Vue 3 Patterns – mit Composition API, Pinia und vollständiger TypeScript-Abdeckung.

Code-Review

Analyse auf veraltete Options-API-Muster, fehlende Typisierung und Performance-Schwachstellen

Migration

Vue 2 auf Vue 3, Vuex auf Pinia, Mixins auf Composables – sauber und inkrementell

Architektur

Komponentendesign, Store-Struktur und Composable-Bibliotheken für wachsende Teams

10. Zusammenfassung

Die wichtigsten Vue 3 Patterns für produktive Frontends lösen immer dasselbe Grundproblem: Code, der ohne klare Muster geschrieben ist, wird in großen Teams zur Blackbox. Die Composition API mit <script setup> ersetzt das verteilte Options-Objekt durch fokussierten, zusammenhängenden Code. Composables extrahieren und teilen Logik ohne Mixin-Kollisionen. defineModel() macht Zwei-Wege-Binding in Formular-Komponenten zu einem Einzeiler. Pinia's Setup-Stores bieten reaktiven globalen State ohne Vuex-Boilerplate. Suspense und async Komponenten machen Lazy-Loading und Lade-Zustände deklarativ statt imperativ.

Der größte Hebel liegt in der konsequenten Anwendung über die gesamte Codebase. Eine Komponente mit modernem Composable-Pattern neben einer anderen mit Options-API und Mixins schafft ungleiche Wartbarkeitsstandards im Team. TypeScript durch alle Schichten – Props, Emits, Composables und Stores – macht Refactoring sicher und Dokumentation teilweise überflüssig, weil die Typen selbst dokumentieren. Wer Vue 3 Patterns konsequent anwendet, bekommt eine Codebase, die mit dem Team und den Anforderungen wächst, statt unter ihnen zu brechen.

Vue 3 Patterns für produktive Frontends — Das Wichtigste auf einen Blick

Composition API

<script setup> mit ref, computed und defineProps – Pflicht in neuen Komponenten. Kein Options-API in neuem Code.

Composables statt Mixins

useFoo()-Funktionen kapseln wiederverwendbare Logik ohne Namenskollisionen, mit vollständiger TypeScript-Inferenz.

Pinia für globalen State

Setup-Store mit ref, computed und Actions – kein Vuex, keine Mutations, volle TS-Unterstützung out of the box.

Performance

shallowRef für große Daten, v-memo für Listen, defineAsyncComponent für Code-Splitting auf Komponentenebene.

11. FAQ: Vue 3 Patterns für produktive Frontends

1Was ist ein Vue 3 Pattern?
Eine bewährte Lösungsstruktur für ein wiederkehrendes Frontend-Problem – nutzt die Composition API gezielt für Wiederverwendbarkeit, Testbarkeit und Typsicherheit.
2Composables statt Mixins?
Mixins kollidieren bei Namensgleichheit und verbergen Herkunft von State. Composables sind explizit, isoliert und vollständig typisierbar.
3ref vs. reactive?
ref für skalare Werte und sicher destrukturierbar. reactive für eng verbundene Objekte – aber nie ohne toRefs() destrukturieren.
4Pinia vs. Composable?
Pinia für globalen geteilten State (Warenkorb, Auth). Composable für lokalen oder pro-Instanz-isolierten State.
5defineModel() erklärt?
Compiler-Makro, das modelValue-Prop und update:modelValue-Emit automatisch registriert. Gibt ein beschreibbares Ref zurück – Props-Down-Events-Up intern erledigt.
6Was bringt Suspense?
Deklaratives Lade-Handling für async setup(). Fallback-Slot solange Promise aussteht – kein isLoading-Boolean mehr in jeder Komponente.
7Reaktivitätsverlust bei Destrukturierung?
toRefs() beim Destrukturieren von reactive-Objekten verwenden. In Composables beim Return-Statement Pflicht.
8shallowRef wann einsetzen?
Für große API-Responses und Datenstrukturen, die als Ganzes ausgetauscht werden. Verhindert teuren Deep-Tracking.
9watch vs. watchEffect?
watch für explizite Quellen und old/new-Werte. watchEffect trackt automatisch alle im Callback genutzten reaktiven Werte.
10defineAsyncComponent für Code-Splitting?
Dynamischer Import erzeugt separaten Chunk. Laden on demand, mit loadingComponent und errorComponent für professionelles UX.