Skip to content

Kapitel 34: Pinia (Teil 2) - State, Getters & Actions

📙 Lernziel: State, Getters & Actions in Pinia meistern!


34.1 State (Zustand)

State ist der reaktive Zustand des Stores.

Beispiel:

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // State (mit ref)
  const count = ref(0)
  
  // State (mit reactive)
  const user = reactive({
    name: 'Max',
    age: 25
  })
  
  return {
    count,
    user
  }
})

Verwendung in Komponente:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    <p>User: {{ counterStore.user.name }}</p>
    
    <!-- State ändern -->
    <button @click="counterStore.count++">+</button>
    <button @click="counterStore.user.age++">Alter erhöhen</button>
  </div>
</template>

Wichtig:

  • ✅ State ist read/write (kann direkt geändert werden)
  • ✅ Besser: Actions verwenden für komplexe Logik

34.2 Getters (Berechnete Eigenschaften)

Getters sind berechnete Eigenschaften (wie computed()).

Beispiel:

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const todos = ref([
    { id: 1, text: 'Lernen', done: false },
    { id: 2, text: 'Sport', done: true }
  ])
  
  // Getters (computed)
  const doubleCount = computed(() => count.value * 2)
  const doneTodos = computed(() => todos.value.filter(todo => todo.done))
  const undoneTodos = computed(() => todos.value.filter(todo => !todo.done))
  
  return {
    count,
    todos,
    doubleCount,
    doneTodos,
    undoneTodos
  }
})

Verwendung in Komponente:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    <p>Doppelt: {{ counterStore.doubleCount }}</p>
    <p>Erledigt: {{ counterStore.doneTodos.length }}</p>
    <p>Offen: {{ counterStore.undoneTodos.length }}</p>
  </div>
</template>

Vorteile:

  • ✅ Caching (wird nur neu berechnet wenn Abhängigkeiten sich ändern)
  • ✅ Wie computed() in Vue

34.3 Actions (Aktionen)

Actions sind Funktionen, die State ändern oder asynchrone Operationen ausführen.

Beispiel:

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const isLoading = ref(false)
  
  // Actions (Funktionen)
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = 0
  }
  
  const incrementBy = (value) => {
    count.value += value
  }
  
  // Asynchrone Action
  const fetchData = async () => {
    isLoading.value = true
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
      const data = await response.json()
      console.log('Daten geladen:', data)
    } catch (error) {
      console.error('Fehler:', error)
    } finally {
      isLoading.value = false
    }
  }
  
  return {
    count,
    isLoading,
    increment,
    decrement,
    reset,
    incrementBy,
    fetchData
  }
})

Verwendung in Komponente:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    
    <button @click="counterStore.increment()">+</button>
    <button @click="counterStore.decrement()">-</button>
    <button @click="counterStore.reset()">Zurücksetzen</button>
    <button @click="counterStore.incrementBy(5)">+5</button>
    <button @click="counterStore.fetchData()">
      {{ counterStore.isLoading ? 'Lädt...' : 'Daten laden' }}
    </button>
  </div>
</template>

Best Practices:

  • ✅ Komplexe Logik in Actions kapseln
  • ✅ Asynchrone Operationen (API-Aufrufe) in Actions
  • ✅ State nur in Actions ändern (nicht direkt in Komponente)

34.4 $reset() - Store zurücksetzen

$reset() setzt Store auf Ausgangswerte zurücksetzen.

Beispiel:

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const user = ref({ name: '', age: 0 })
  
  const reset = () => {
    count.value = 0
    user.value = { name: '', age: 0 }
  }
  
  return {
    count,
    user,
    reset
  }
})

Verwendung:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    <button @click="counterStore.count++">+</button>
    <button @click="counterStore.reset()">Zurücksetzen</button>
  </div>
</template>

Hinweis:

  • ❌ In Setup Store gibt es kein automatisches $reset(). Du musst es selbst implementieren.
  • ✅ In Options Store gibt es automatisches $reset().

34.5 $patch() - State in Batches ändern

$patch() ändert mehrere State-Eigenschaften auf einmal (effizienter).

Beispiel:

javascript
// Stores/counter.js (Options Store)
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: {
      name: 'Max',
      age: 25
    }
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

Verwendung:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    <p>User: {{ counterStore.user.name }}, {{ counterStore.user.age }}</p>
    
    <!-- Einzelne Änderung -->
    <button @click="counterStore.count++">+</button>
    
    <!-- Mehrere Änderungen auf einmal (effizienter) -->
    <button @click="counterStore.$patch({ count: 0, user: { name: 'Anna', age: 30 } })">
      Alles zurücksetzen
    </button>
    
    <!-- Mit Funktion (für komplexe Logik) -->
    <button @click="counterStore.$patch((state) => {
      state.count = 0
      state.user.name = 'Tom'
      state.user.age = 40
    })">
      Alles zurücksetzen (Funktion)
    </button>
  </div>
</template>

Vorteile:

  • ✅ Effizienter (nur ein Re-render)
  • ✅ Bessere Performance bei mehreren Änderungen

34.6 $subscribe() - State-Änderungen überwachen

$subscribe() erlaubt es, auf State-Änderungen zu reagieren (z.B. für LocalStorage-Persistenz).

Beispiel:

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  
  // State in LocalStorage persistieren
  const storedCount = localStorage.getItem('count')
  if (storedCount) {
    count.value = JSON.parse(storedCount)
  }
  
  // Änderungen überwachen und in LocalStorage speichern
  const unsubscribe = $subscribe((mutation, state) => {
    console.log('Mutation:', mutation.type)
    console.log('Neuer State:', state)
    localStorage.setItem('count', JSON.stringify(state.count))
  })
  
  const increment = () => {
    count.value++
  }
  
  return {
    count,
    increment
  }
})

Verwendung:

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()

// Beim Unmounten unsubscribe (Cleanup)
onUnmounted(() => {
  counterStore.$dispose() // Entfernt alle Subscriber
})
</script>

<template>
  <div>
    <p>Zähler: {{ counterStore.count }}</p>
    <button @click="counterStore.increment()">+</button>
    <p>(Wert wird in LocalStorage gespeichert!)</p>
  </div>
</template>

Anwendungsfälle:

  • ✅ State in LocalStorage persistieren
  • ✅ Analytics/Logging bei State-Änderungen
  • ✅ Synchronisation mit Server

34.7 Übung: Todo Store mit Pinia (Erweitert)

Aufgabe: Erstelle einen Todo Store mit State, Getters und Actions.

Lösung:

1. Store erstellen (stores/todo.js):

javascript
// stores/todo.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  // State
  const todos = ref([
    { id: 1, text: 'Lernen', done: false },
    { id: 2, text: 'Sport', done: true }
  ])
  const filter = ref('all') // 'all', 'done', 'undone'
  
  // Getters
  const doneTodos = computed(() => todos.value.filter(todo => todo.done))
  const undoneTodos = computed(() => todos.value.filter(todo => !todo.done))
  
  const filteredTodos = computed(() => {
    if (filter.value === 'done') {
      return doneTodos.value
    } else if (filter.value === 'undone') {
      return undoneTodos.value
    }
    return todos.value
  })
  
  // Actions
  const addTodo = (text) => {
    todos.value.push({
      id: Date.now(),
      text,
      done: false
    })
  }
  
  const removeTodo = (id) => {
    todos.value = todos.value.filter(todo => todo.id !== id)
  }
  
  const toggleTodo = (id) => {
    const todo = todos.value.find(t => t.id === id)
    if (todo) {
      todo.done = !todo.done
    }
  }
  
  const setFilter = (newFilter) => {
    filter.value = newFilter
  }
  
  const reset = () => {
    todos.value = []
    filter.value = 'all'
  }
  
  return {
    todos,
    filter,
    doneTodos,
    undoneTodos,
    filteredTodos,
    addTodo,
    removeTodo,
    toggleTodo,
    setFilter,
    reset
  }
})

2. Komponente erstellen (TodoList.vue):

vue
<!-- components/TodoList.vue -->
<script setup>
import { ref } from 'vue'
import { useTodoStore } from '../stores/todo'

const todoStore = useTodoStore()
const newTodo = ref('')

const addTodo = () => {
  if (newTodo.value.trim() !== '') {
    todoStore.addTodo(newTodo.value)
    newTodo.value = ''
  }
}
</script>

<template>
  <div class="todo-list">
    <h2>Todo Liste</h2>
    
    <!-- Eingabe -->
    <div>
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo()"
        placeholder="Neue Aufgabe..."
      />
      <button @click="addTodo()">Hinzufügen</button>
    </div>
    
    <!-- Filter -->
    <div>
      <button @click="todoStore.setFilter('all')" :class="{ active: todoStore.filter === 'all' }">Alle</button>
      <button @click="todoStore.setFilter('done')" :class="{ active: todoStore.filter === 'done' }">Erledigt</button>
      <button @click="todoStore.setFilter('undone')" :class="{ active: todoStore.filter === 'undone' }">Offen</button>
    </div>
    
    <!-- Liste -->
    <ul>
      <li v-for="todo in todoStore.filteredTodos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.done" 
          @change="todoStore.toggleTodo(todo.id)"
        />
        <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">
          {{ todo.text }}
        </span>
        <button @click="todoStore.removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>
    
    <!-- Statistik -->
    <p>Gesamt: {{ todoStore.todos.length }}</p>
    <p>Erledigt: {{ todoStore.doneTodos.length }}</p>
    <p>Offen: {{ todoStore.undoneTodos.length }}</p>
    
    <button @click="todoStore.reset()">Zurücksetzen</button>
  </div>
</template>

<style scoped>
.todo-list {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.active {
  background: #42b883;
  color: white;
}

button {
  margin: 0 5px;
  padding: 5px 10px;
  cursor: pointer;
}

li {
  margin: 10px 0;
}
</style>

✅ Zusammenfassung

In diesem Kapitel hast du gelernt:

  • ✅ State (Zustand) in Pinia
  • ✅ Getters (berechnete Eigenschaften)
  • ✅ Actions (Aktionen)
  • $reset() (Store zurücksetzen)
  • $patch() (State in Batches ändern)
  • $subscribe() (State-Änderungen überwachen)
  • ✅ Praxis: Todo Store mit Pinia (erweitert)

🎯 Nächster Schritt: In Kapitel 35 lernst du Datenpersistenz (LocalStorage)!


← Zurück zu Kapitel 33: Pinia GrundlagenWeiter zu Kapitel 35: Datenpersistenz →

Frei für alle Anfänger