Was ist Pinia?
Pinia ist die offizielle State-Management-Library für Vue. Sie hat Vuex als empfohlener Store in Vue 3 abgelöst und ist mittlerweile der Standard in neuen Vue- und Nuxt-Projekten. Pinia ist leichtgewichtig, typsicher und direkt auf der Composition API aufgebaut.
Das Design ist bewusst minimalistisch gehalten. Ein Store ist eine Funktion, die State, abgeleitete Werte und Actions zurückgibt. Es gibt keine Mutations, kein Modul-Namespacing und keinen Provider pro Store. Wenn du ref, computed und Funktionen kennst, beherrschst du bereits den Großteil von Pinia.
Einen Store definieren
Ein Store wird mit defineStore erstellt, wobei eine eindeutige ID und eine Setup-Funktion übergeben werden.
// stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0),
);
const count = computed(() => items.value.length);
function add(item) {
items.value.push(item);
}
function remove(id) {
items.value = items.value.filter((item) => item.id !== id);
}
return { items, total, count, add, remove };
});
Dies ist ein Setup Store: ref wird zum State, computed wird zu einem Getter und Funktionen werden zu Actions. Die eindeutige ID benennt den Store in den Devtools und ist erforderlich.
Pinia unterstützt auch einen Options Store, falls Sie die Vuex-ähnliche Struktur bevorzugen:
// stores/counter.js
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
});
Beide Formen werden vollständig unterstützt. Setup Stores passen tendenziell besser zu TypeScript und der Composition API, während sich Options Stores für Vuex-Nutzer vertrauter anfühlen können.
Verwendung eines Stores in Komponenten
Rufen Sie die Store-Funktion auf, um die Store-Instanz zu erhalten. Es gibt keinen Provider, mit dem Sie Ihre Komponenten umschließen müssen.
<!-- Cart.vue -->
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} — {{ item.price }}
<button @click="cart.remove(item.id)">Remove</button>
</li>
</ul>
<p>Total: {{ total }}</p>
</template>
Das direkte Lesen von cart.total in einem Template bleibt reaktiv. Wenn Sie State oder Getter destrukturieren möchten und dabei die Reaktivität beibehalten wollen, verwenden Sie storeToRefs, welches diese in refs konvertiert. Actions können direkt destrukturiert werden, da sie keine Reaktivität benötigen.
Getter
Getter sind berechnete Werte, die aus dem State abgeleitet werden. Sie werden gecacht und gemeinsam genutzt, sodass jede Komponente, die denselben Getter liest, dieselbe Berechnung verwendet.
// stores/products.js
export const useProductsStore = defineStore("products", () => {
const products = ref([]);
const filter = ref("");
const visible = computed(() =>
products.value.filter((p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
),
);
const inStock = computed(() =>
visible.value.filter((p) => p.stock > 0),
);
return { products, filter, visible, inStock };
});
Es ist ein häufiger Fehler, auf watch zurückzugreifen, um einen Wert zu berechnen. Wenn dieser aus dem bestehenden State abgeleitet werden kann, sollte er ein Getter sein – genau so, wie Sie computed in einer Komponente verwenden würden.
Actions
Actions sind Funktionen, die den State ändern. Sie können synchron oder asynchron sein und können andere Actions oder sogar andere Stores aufrufen.
// stores/users.js
export const useUsersStore = defineStore("users", () => {
const users = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
return { users, loading, error, fetchUsers };
});
Für Server-Daten empfiehlt es sich jedoch, eine Data-Fetching-Layer wie TanStack Query zu verwenden, welche das Caching und die Invalidierung übernimmt. Pinia eignet sich am besten für den Client-State: Warenkörbe, Filter, UI-Einstellungen, Auth-State und Entwürfe.
Plugins und Persistenz
Pinia-Plugins werden für jeden Store ausgeführt und können Funktionen wie Persistenz, Logging oder das Zurücksetzen von Zuständen hinzufügen.
// persist.js
export function persistPlugin({ store }) {
const saved = localStorage.getItem(store.$id);
if (saved) store.$patch(JSON.parse(saved));
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// main.js
const pinia = createPinia();
pinia.use(persistPlugin);
app.use(pinia);
Das beliebte pinia-plugin-persistedstate bietet dieselbe Funktionalität mit Optionen dazu, welche Keys gespeichert werden sollen. Persistenz ist einer der Hauptgründe, warum man einen Store anstelle eines lokalen Component-States verwendet.
Pinia mit Nuxt
Installiere in Nuxt das offizielle Modul; Stores in stores/ werden dann automatisch importiert.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>
Da Nuxt die Installation und das Server-Rendering übernimmt, funktioniert derselbe Store sowohl auf dem Server als auch auf dem Client. Siehe den Nuxt-Guide dazu, wie dies mit useFetch und Server-Routes zusammenpasst.
Best Practices
- Bevorzugen Sie Setup Stores für neuen Code, insbesondere in Verbindung mit TypeScript.
- Halten Sie abgeleitete Daten in Gettern, anstatt den State zu beobachten.
- Verwenden Sie
storeToRefsbeim Destrukturieren von State oder Gettern. - Behalten Sie Server-Daten in einem Data-Fetching-Layer und nicht in Pinia.
- Nutzen Sie Actions für alles, was den State ändert, einschließlich asynchroner Operationen.
- Persistieren Sie nur den State, der einen Reload überstehen soll.
- Benennen Sie Stores klar nach der Domäne, die sie verwalten.
Häufige Fehler
- Destructuring des State ohne
storeToRefs, wodurch die Reaktivität verloren geht. - Verwendung von
watchzur Berechnung eines Wertes, den eigentlich ein Getter ableiten sollte. - Speichern von abgerufenen Server-Daten im Store und manuelles Verwalten von Loading-Flags.
- Vergessen, Pinia zu installieren, sodass Store-Aufrufe zur Laufzeit fehlschlagen.
- Erstellung eines einzigen riesigen Stores anstelle von fokussierten Domain-Stores.
- Mutation des State außerhalb einer Action, was die Nachverfolgung erschwert.
Wie geht es weiter?
Pinia ist das Vue-Äquivalent zu einem kleinen, modernen Store. Vertiefe dein Vue-Wissen, ergänze Nuxt für Server Rendering und Auto-Imports und vergleiche diesen Ansatz mit der Context API, Zustand und Redux Toolkit von React. Erstelle anschließend einen kleinen Store mit einem Getter, einer async Action und Persistence, um zu sehen, wie wenig Code dafür nötig ist.