
Pinia ist die empfohlene Bibliothek für State Management in Vue 3, Vuex ist im Wartungsmodus. Einen Store deklarierst du mit defineStore, nutzt ihn, indem du usePanierStore() in einer beliebigen Komponente aufrufst, und destrukturierst ihn mit storeToRefs, um die Reaktivität nicht zu verlieren.
Ein Event von einer Komponente an ihr Elternteil weiterzureichen, funktioniert über ein oder zwei Ebenen gut. Darüber hinaus verbringst du deine Zeit damit, Events erneut zu emittieren und Props durch Komponenten zu reichen, die nichts damit anfangen. Pinia löst genau das: ein Zustand, einmal deklariert, aus jeder Komponente lesbar und änderbar.
Pinia oder Vuex?
Die Frage stellt sich nicht mehr. Vuex ist im Wartungsmodus, und die offizielle Vue-Dokumentation verweist auf Pinia, die inzwischen empfohlene Bibliothek für State Management. Für ein neues Projekt nimmst du ohne Zögern Pinia. Für ein laufendes Vuex-Projekt eilt die Migration nicht: Sie läuft Store für Store, beide vertragen sich nebeneinander.
Was Pinia in der Praxis ändert: keine Mutations mehr, nur noch Actions; keine verschachtelten Module mehr, jeder Store ist ein Modul, und eine Typisierung, die ohne Verrenkungen funktioniert.
Installieren
npm install piniaDann hängst du das Plugin in die Anwendung ein:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')Bei einem neuen Projekt übernimmt der offizielle Generator das: Antworte bei npm create vue@latest mit „ja“ auf die Pinia-Frage.
Einen Store schreiben
Einen Store deklarierst du mit defineStore. Das erste Argument ist ein eindeutiger Identifier, den die Devtools verwenden. Das zweite kann zwei Formen annehmen, die Setup-Form ist die, die das offizielle Tooling erzeugt, und die, die einer Komponente am nächsten kommt.
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const usePanierStore = defineStore('panier', () => {
// State: refs
const lignes = ref([])
// Getter: computed
const total = computed(() =>
lignes.value.reduce((somme, l) => somme + l.prix * l.quantite, 0)
)
const nombreArticles = computed(() =>
lignes.value.reduce((somme, l) => somme + l.quantite, 0)
)
// Actions: Funktionen
function ajouter(produit) {
const ligne = lignes.value.find((l) => l.id === produit.id)
if (ligne) {
ligne.quantite += 1
return
}
lignes.value.push({ ...produit, quantite: 1 })
}
function retirer(id) {
lignes.value = lignes.value.filter((l) => l.id !== id)
}
function vider() {
lignes.value = []
}
return { lignes, total, nombreArticles, ajouter, retirer, vider }
})Alles, was die Funktion zurückgibt, wird öffentlich. Was sie nicht zurückgibt, bleibt privat im Store: So behältst du dort einen internen Zähler oder eine Hilfsfunktion für dich.
Die Namenskonvention lautet useQuelqueChoseStore, wie bei einem Composable.
Die Options-Form
Die andere Schreibweise übernimmt die Struktur von Vuex, ohne die Mutations:
export const usePanierStore = defineStore('panier', {
state: () => ({ lignes: [] }),
getters: {
total: (state) => state.lignes.reduce((s, l) => s + l.prix * l.quantite, 0)
},
actions: {
ajouter(produit) {
this.lignes.push({ ...produit, quantite: 1 })
}
}
})Beide Formen sind gleichwertig und austauschbar. Die Options-Form erleichtert den Umstieg von Vuex, die Setup-Form gibt mehr Freiheit, etwa um ein Composable innerhalb des Stores zu verwenden.
Den Store benutzen
Ruf die Funktion in der Komponente auf. Die Instanz ist einzigartig: Zwei Komponenten, die usePanierStore() aufrufen, bekommen exakt dasselbe Objekt.
<script setup>
import { usePanierStore } from '@/stores/panier'
const panier = usePanierStore()
</script>
<template>
<p>{{ panier.nombreArticles }} article(s) — {{ panier.total }} €</p>
<ul>
<li v-for="ligne in panier.lignes" :key="ligne.id">
{{ ligne.nom }} × {{ ligne.quantite }}
<button @click="panier.retirer(ligne.id)">Retirer</button>
</li>
</ul>
<button @click="panier.vider()">Vider</button>
</template>State, Getter und Actions erreichst du direkt auf dem Objekt, ohne Umweg über einen Vermittler.
storeToRefs, oder die verlorene Reaktivität
Das ist der häufigste Fehler mit Pinia. Den Store zu destrukturieren liefert eingefrorene Werte:
const panier = usePanierStore()
const { total } = panier // ✗ eingefrorene Zahl, ändert sich nicht mehr
panier.ajouter({ id: 1, prix: 7 })
console.log(total) // 0storeToRefs extrahiert State und Getter und erhält dabei die Reaktivität:
import { storeToRefs } from 'pinia'
const panier = usePanierStore()
const { total, lignes } = storeToRefs(panier) // ✓ refs
const { ajouter, retirer } = panier // ✓ Actions lassen sich destrukturieren
panier.ajouter({ id: 1, prix: 7 })
console.log(total.value) // 7Merk dir die Regel: State und Getter laufen über storeToRefs, Actions destrukturierst du direkt. Actions sind schlichte Funktionen, die an den Store gebunden sind, sie verlieren dabei nichts.
Den State verändern
Drei Wege, geordnet nach Vorzug.
Eine Action für jede Fachlogik. Das ist der Normalfall: Die Regel steht einmal geschrieben im Store.
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })$patch, um mehrere Einträge in einem einzigen Vorgang zu ändern, was in den Devtools nur einen Eintrag erzeugt:
panier.$patch({ lignes: [], codePromo: null })
// Funktionsform, für Arrays
panier.$patch((state) => {
state.lignes.push(nouvelleLigne)
state.derniereMaj = Date.now()
})Die direkte Zuweisung ist erlaubt, anders als bei Vuex, aber heb sie dir für triviale Fälle auf:
panier.codePromo = 'RENTREE'Bei einem Store in Options-Form stellt panier.$reset() den Ausgangszustand wieder her. Bei einem Store in Setup-Form kennt Pinia diesen Ausgangszustand nicht: Der Aufruf wirft einen Fehler. Schreib deine eigene Action zum Zurücksetzen, so wie die Funktion vider() weiter oben.
Änderungen beobachten
$subscribe wird bei jeder Änderung des State aufgerufen. Das ist der Hook, um den Warenkorb zu persistieren:
panier.$subscribe((mutation, state) => {
localStorage.setItem('panier', JSON.stringify(state.lignes))
})Für einen breiteren Persistenzbedarf erledigt ein Pinia-Plugin die Arbeit über alle Stores hinweg. Ein Plugin ist eine Funktion, die den Kontext erhält und jeden Store erweitert:
export function persistance({ store }) {
const sauvegarde = localStorage.getItem(store.$id)
if (sauvegarde) {
store.$patch(JSON.parse(sauvegarde))
}
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}const pinia = createPinia()
pinia.use(persistance)Einen Store testen
Einen Store testest du, ohne eine Komponente zu mounten. Du musst nur vor jedem Test eine frische Pinia-Instanz aktivieren, um von einem leeren Zustand aus zu starten:
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { usePanierStore } from '../panier.js'
describe('store panier', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('cumule les quantités du même produit', () => {
const panier = usePanierStore()
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })
expect(panier.lignes).toHaveLength(1)
expect(panier.nombreArticles).toBe(2)
expect(panier.total).toBe(50)
})
})Ohne setActivePinia schlägt der Aufruf des Stores mit der Meldung „“getActivePinia()“ was called but there was no active Pinia. Are you trying to use a store before calling „app.use(pinia)“?“ fehl.
Wann du Pinia nicht einsetzen solltest
Ein Store ist nicht umsonst: Er bringt eine Datei mehr, eine Indirektion und einen weiteren globalen Zustand mit. Er lohnt sich, wenn Daten von weit auseinanderliegenden Komponenten genutzt werden oder das Unmounten einer Komponente überleben müssen.
Für Daten aus einem Echtzeitdienst schaust du dir besser Firebase und VueFire an, deren Bindings bereits reaktiv sind.
Für den lokalen Zustand eines Formulars genügt ein ref. Für einen Zustand, den ein Elternteil mit seinem direkten Kind teilt, erledigen Props und Events die Arbeit. Für das Caching von Serverdaten passt ein spezialisiertes Werkzeug wie TanStack Query besser.
Wenn Vue neu für dich ist, fang mit dem Vue-3-Tutorial für Einsteiger an: Pinia ergibt erst dann richtig Sinn, wenn Komponenten und Events sitzen.
Häufige Fehler
const { total } = panier liefert einen eingefrorenen Wert. Nimm storeToRefs(panier) für State und Getter, Actions dagegen destrukturierst du direkt.n$reset auf einem Setup-Store | Pinia kennt den Ausgangszustand eines in Setup-Form geschriebenen Stores nicht: Der Aufruf wirft einen Fehler. Schreib deine eigene Action zum Zurücksetzen.setActivePinia(createPinia()) auf.

