Modul 2: Vue Grundlagen

Lifecycle Hooks

Den Lebenszyklus einer Vue-Komponente verstehen und gezielt nutzen

Was sind Lifecycle Hooks?

Jede Vue-Komponente durchläuft einen klar definierten Lebenszyklus: Sie wird erstellt, ins DOM eingehängt (mounted), bei Zustandsänderungen aktualisiert und schließlich wieder ausgehängt (unmounted). Lifecycle Hooks sind Funktionen, mit denen du Code zu genau diesen Zeitpunkten ausführen kannst.

Stell dir das wie Callbacks vor, die Vue automatisch aufruft, wenn eine Komponente eine bestimmte Phase erreicht. Du registrierst sie innerhalb von <script setup> und Vue kümmert sich um den Rest.

ℹ️

Lifecycle Hooks auf einen Blick

Die wichtigsten Hooks in der Composition API sind: onMounted, onUpdated und onUnmounted. Für die meisten Anwendungsfälle brauchst du nur diese drei. Die „Before"-Varianten (onBeforeMount, onBeforeUpdate, onBeforeUnmount) sind seltener nötig, aber gut zu kennen.

Der Lifecycle im Überblick

Der vollständige Lebenszyklus einer Vue-Komponente lässt sich als Abfolge von Phasen und Hooks darstellen:

── Komponente wird erstellt ──

📦 setup() ← hier läuft dein <script setup>-Code

── DOM wird vorbereitet ──

onBeforeMount ← DOM existiert noch nicht

onMounted ← DOM ist verfügbar!

── Reaktive Daten ändern sich ──

onBeforeUpdate ← DOM zeigt noch alten Zustand

🔄 onUpdated ← DOM ist aktualisiert

── Komponente wird entfernt ──

onBeforeUnmount ← DOM ist noch da

💀 onUnmounted ← Komponente ist weg, aufräumen!

Beachte: setup() ist kein Hook, den du registrierst — er ist der Kontext, in dem du die Hooks registrierst. Dein gesamter <script setup>-Code läuft in der setup-Phase.

LifecycleLogger.vue
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

// 1. setup() — läuft jetzt, synchron
console.log('setup: Komponente wird initialisiert')

onBeforeMount(() => {
  console.log('onBeforeMount: DOM wird gleich erstellt')
})

onMounted(() => {
  console.log('onMounted: DOM ist verfügbar! 🎉')
})

onBeforeUpdate(() => {
  console.log('onBeforeUpdate: State hat sich geändert, DOM wird gleich aktualisiert')
})

onUpdated(() => {
  console.log('onUpdated: DOM wurde aktualisiert')
})

onBeforeUnmount(() => {
  console.log('onBeforeUnmount: Komponente wird gleich entfernt')
})

onUnmounted(() => {
  console.log('onUnmounted: Komponente entfernt, Tschüss! 👋')
})
</script>

onMounted

onMounted ist der mit Abstand am häufigsten verwendete Lifecycle Hook. Er wird aufgerufen, sobald die Komponente ins DOM eingehängt wurde. Das bedeutet: Du hast Zugriff auf DOM-Elemente, kannst Daten laden oder Drittanbieter-Bibliotheken initialisieren.

Typische Anwendungsfälle

  • Daten von einer API laden
  • DOM-Elemente messen (Breite, Höhe, Position)
  • Drittanbieter-Bibliotheken initialisieren (z.B. Charts, Maps)
  • Event Listener auf window oder document registrieren
UserList.vue
<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(true)
const error = ref(null)

onMounted(async () => {
  try {
    const response = await fetch('/api/users')
    users.value = await response.json()
  } catch (err) {
    error.value = 'Benutzer konnten nicht geladen werden'
    console.error(err)
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <div v-if="loading" class="spinner">Lade Benutzer…</div>
  <div v-else-if="error" class="error">{{ error }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>
💡

Nuxt-Tipp: useFetch statt manuellem fetch

In Nuxt bevorzuge useFetch() oder useAsyncData() statt manuellem fetch in onMounted. Diese Composables handhaben SSR, Caching und automatische Reaktivität für dich. Du sparst dir Loading-States, Error-Handling und Race Conditions.

onUpdated

onUpdated wird aufgerufen, nachdem eine reaktive Zustandsänderung das DOM aktualisiert hat. Das ist nützlich, wenn du nach einem Re-Render auf das aktualisierte DOM zugreifen musst — zum Beispiel um Scroll-Positionen anzupassen oder DOM-Messungen durchzuführen.

AutoScrollChat.vue
<script setup>
import { ref, onUpdated } from 'vue'

const messages = ref([])
const chatContainer = ref(null)

// Nach jedem DOM-Update: ans Ende scrollen
onUpdated(() => {
  if (chatContainer.value) {
    chatContainer.value.scrollTop = chatContainer.value.scrollHeight
  }
})

function addMessage(text) {
  messages.value.push({
    id: Date.now(),
    text,
    time: new Date().toLocaleTimeString('de-DE')
  })
}
</script>

<template>
  <div ref="chatContainer" class="chat-container">
    <div v-for="msg in messages" :key="msg.id" class="message">
      <span class="time">{{ msg.time }}</span>
      <p>{{ msg.text }}</p>
    </div>
  </div>
</template>
⚠️

Vorsicht: Endlosschleifen!

Ändere niemals reaktiven State direkt in onUpdated! Das löst einen neuen Render aus, der wiederum onUpdated auslöst — eine Endlosschleife. Wenn du State basierend auf DOM-Änderungen setzen musst, verwende nextTick() oder prüfe mit einer Bedingung, ob die Änderung wirklich nötig ist.

onUnmounted

onUnmounted ist dein Aufräum-Hook. Hier entfernst du alles, was du in onMounted eingerichtet hast: Event Listener, Intervalle, Timeouts, WebSocket-Verbindungen, Intersection Observers. Vergisst du das Aufräumen, hast du Memory Leaks.

LiveClock.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const currentTime = ref('')
let intervalId = null  // kein ref — wird nicht im Template gebraucht

onMounted(() => {
  // Intervall starten: Uhrzeit jede Sekunde aktualisieren
  intervalId = setInterval(() => {
    currentTime.value = new Date().toLocaleTimeString('de-DE')
  }, 1000)

  // Sofort einmal setzen
  currentTime.value = new Date().toLocaleTimeString('de-DE')
})

onUnmounted(() => {
  // Intervall aufräumen — sonst Memory Leak!
  if (intervalId) {
    clearInterval(intervalId)
    intervalId = null
  }
})
</script>

<template>
  <div class="live-clock">
    🕐 {{ currentTime }}
  </div>
</template>

Beachte das Muster: Was in onMounted aufgebaut wird, wird in onUnmounted wieder abgebaut. Dieses Setup/Cleanup-Pattern ist eines der wichtigsten Konzepte im Umgang mit Lifecycle Hooks.

Die „Before"-Hooks

Neben den Haupt-Hooks gibt es jeweils eine „Before"-Variante: onBeforeMount, onBeforeUpdate und onBeforeUnmount. Sie laufen bevor Vue das DOM aktualisiert — du hast also noch Zugriff auf den vorherigen Zustand.

Wann brauchst du sie?

  • onBeforeUpdate: Snapshot des DOM-Zustands vor einem Update (z.B. Scroll-Position merken)
  • onBeforeUnmount: Letzte Aktionen vor dem Entfernen, wenn du noch DOM-Zugriff brauchst
  • onBeforeMount: Selten benötigt — setup() reicht meistens aus
ScrollPreserver.vue
<script setup>
import { ref, onBeforeUpdate, onUpdated } from 'vue'

const items = ref(['Eintrag 1', 'Eintrag 2'])
const listRef = ref(null)
let previousScrollHeight = 0

// Vor dem Update: aktuellen Scroll-Zustand merken
onBeforeUpdate(() => {
  if (listRef.value) {
    previousScrollHeight = listRef.value.scrollHeight
  }
})

// Nach dem Update: Scroll-Position anpassen
onUpdated(() => {
  if (listRef.value) {
    const newScrollHeight = listRef.value.scrollHeight
    const heightDiff = newScrollHeight - previousScrollHeight
    listRef.value.scrollTop += heightDiff
  }
})

function addItem() {
  items.value.unshift(`Eintrag ${items.value.length + 1}`)
}
</script>

Häufige Patterns

In der Praxis wirst du immer wieder auf die gleichen Lifecycle-Muster stoßen. Hier sind die wichtigsten:

1. Fetch on Mount mit Loading State

Das Standardmuster für Datenladung: Zeige einen Ladezustand, lade Daten in onMounted, und aktualisiere den State.

DataLoader.vue
<script setup>
import { ref, onMounted } from 'vue'

// State
const data = ref(null)
const loading = ref(true)
const error = ref(null)

// Daten laden, sobald die Komponente eingehängt ist
onMounted(async () => {
  try {
    const res = await fetch('https://api.example.com/articles')
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    data.value = await res.json()
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <div v-if="loading">⏳ Wird geladen…</div>
  <div v-else-if="error" class="text-red-500">
    ❌ Fehler: {{ error }}
  </div>
  <article v-else v-for="article in data" :key="article.id">
    <h2>{{ article.title }}</h2>
    <p>{{ article.excerpt }}</p>
  </article>
</template>

2. Setup + Cleanup (Event Listener)

Immer wenn du in onMounted etwas registrierst, räume es in onUnmounted auf. Das gilt für Event Listener, WebSockets, Intervalle und Observer.

WindowResize.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const windowWidth = ref(window.innerWidth)

function handleResize() {
  windowWidth.value = window.innerWidth
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

<template>
  <p>Fensterbreite: {{ windowWidth }}px</p>
</template>

3. Scroll-Position wiederherstellen

Nutze onBeforeUpdate, um die Scroll-Position vor einem Update zu speichern, und onUpdated, um sie wiederherzustellen:

ChatMessages.vue
<script setup>
import { ref, onBeforeUpdate, onUpdated } from 'vue'

const messages = ref([])
const container = ref(null)
let savedScrollTop = 0
let savedScrollHeight = 0

onBeforeUpdate(() => {
  if (container.value) {
    savedScrollTop = container.value.scrollTop
    savedScrollHeight = container.value.scrollHeight
  }
})

onUpdated(() => {
  if (container.value) {
    // Neue Nachrichten oben eingefügt?
    // → Scroll-Position korrigieren, damit der Inhalt nicht springt
    const heightDiff = container.value.scrollHeight - savedScrollHeight
    container.value.scrollTop = savedScrollTop + heightDiff
  }
})
</script>

Rails-Vergleich

Als Rails-Entwickler kennst du Callbacks aus zwei Kontexten: Controller-Callbacks wie before_action und ActiveRecord-Callbacks wie before_save. Vue Lifecycle Hooks funktionieren nach einem ähnlichen Prinzip — aber in einem völlig anderen Kontext.

🛤️

Rails Callbacks vs. Vue Lifecycle Hooks

Gemeinsamkeiten: Beide ermöglichen dir, Code zu bestimmten Zeitpunkten im Lebenszyklus eines Objekts auszuführen. Das Muster „vorher → Aktion → nachher" existiert in beiden Welten.

RailsVueAnmerkung
before_actiononBeforeMountSetup vor der Hauptaktion
after_actiononMountedAktion abgeschlossen, Ergebnis verfügbar
after_saveonUpdatedZustand hat sich geändert und ist persistiert
before_destroyonBeforeUnmountLetzte Chance vor dem Aufräumen

Wichtiger Unterschied: Rails-Callbacks sind serverseitig und an einen HTTP-Request oder Datenbankoperation gebunden. Vue Hooks sind clientseitig und an den Lebenszyklus einer UI-Komponente im Browser gebunden. Ein Rails-Controller wird pro Request erstellt und verworfen — eine Vue-Komponente lebt so lange, wie sie im DOM eingehängt ist.

Interaktive Demo

In dieser Demo siehst du die Lifecycle Hooks in Aktion. Klicke auf die Buttons und beobachte, welche Hooks in welcher Reihenfolge ausgelöst werden.

Lifecycle-Hook-Logger
Interaktiv
🔴 Kind-Komponente ist nicht eingehängt

// Lifecycle-Log:

Noch keine Events. Klicke „Komponente einhängen" zum Starten.

ℹ️

Hinweis zur Demo

In einer echten Anwendung wäre die Kind-Komponente eine eigene .vue-Datei mit eigenen Lifecycle Hooks. Hier simulieren wir das Verhalten innerhalb einer Seite, damit du das Prinzip direkt sehen kannst, ohne zwischen Dateien wechseln zu müssen.