Wat is Pinia?
Pinia is Vue's officiële library voor state management. Je gebruikt het wanneer je state hebt die door meerdere componenten in je app gebruikt moet worden.
- Ingelogde gebruiker
- Winkelmandje
- Theme (dark/light)
- Notificaties / toasts
- Globale settings
Wanneer geen Pinia?
- State alleen in één component → gewone
ref - State binnen een component-tree → provide/inject
- State globaal? → Pinia
Vue vs React: Pinia is Vue's equivalent van React's Context + useReducer of libraries als Zustand/Redux. Zelfde doel, simpelere API.
Installeren
Heb je tijdens npm create vue@latest "Yes" gekozen bij Pinia? Dan staat alles al klaar. Anders:
npm install pinia
Activeren in main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Vergeet app.use(createPinia()) niet. Zonder deze regel kun je stores gebruiken, maar werken ze niet correct.
Je eerste store
Maak een src/stores/ folder. Per store één bestand.
Counter store — voorbeeld
// src/stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
// Computed (afgeleide waarde)
const double = computed(() => count.value * 2)
// Actions
const increment = () => {
count.value++
}
const reset = () => {
count.value = 0
}
// ALLES wat je teruggeeft wordt deel van de store
return { count, double, increment, reset }
})
Wat zit erin? Het is gewoon een composable! Refs, computeds, functies. Het verschil: door defineStore wordt het een gedeelde instance — elke component die useCounterStore() aanroept ziet dezelfde state.
Twee dingen om te onthouden:
- Bestandsnaam =
useXxxStore - Eerste argument van
defineStoreis een unieke ID (string)
Gebruiken in een component
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
<template>
<p>Count: {{ store.count }}</p>
<p>Dubbel: {{ store.double }}</p>
<button @click="store.increment">+1</button>
<button @click="store.reset">Reset</button>
</template>
Het mooie: elke component die useCounterStore() aanroept krijgt dezelfde store. Verander de count in component A — component B ziet het direct.
Direct state aanpassen
Buiten een action kun je state ook direct muteren:
store.count = 100 // ✅ mag
store.count++ // ✅ mag
Maar: voor herbruikbare logica is een action netter. Verspreid state-mutaties niet door je hele app.
Compleet voorbeeld — winkelmandje
De store
// src/stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalItems = computed(() =>
items.value.reduce((sum, i) => sum + i.qty, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
const addItem = (product) => {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.qty++
} else {
items.value.push({ ...product, qty: 1 })
}
}
const removeItem = (id) => {
items.value = items.value.filter(i => i.id !== id)
}
const clear = () => {
items.value = []
}
return { items, totalItems, totalPrice, addItem, removeItem, clear }
})
Product-component voegt toe
<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
defineProps(['product'])
const cart = useCartStore()
</script>
<template>
<div>
<h3>{{ product.name }}</h3>
<p>€ {{ product.price }}</p>
<button @click="cart.addItem(product)">In mandje</button>
</div>
</template>
CartView leest de zelfde state
<!-- CartView.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<h1>Winkelmandje ({{ cart.totalItems }})</h1>
<ul>
<li v-for="item in cart.items" :key="item.id">
{{ item.name }} × {{ item.qty }}
<button @click="cart.removeItem(item.id)">×</button>
</li>
</ul>
<p>Totaal: € {{ cart.totalPrice.toFixed(2) }}</p>
<button @click="cart.clear">Mandje legen</button>
</template>
Wat heb je nu? Producten op de ene pagina, mandje op een andere, header-icon met cart.totalItems — allemaal kijken naar dezelfde state. Geen props, geen events naar boven sturen.
Belangrijke Regels
- Eén store per bestand in
src/stores/ - Bestandsnaam & functienaam:
useXxxStore - Eerste argument van
defineStoreis een unieke ID-string - Roep
useXxxStore()aan in<script setup>, niet op top-level van JS-bestanden - Activeer Pinia in
main.jsmetapp.use(createPinia())
Veelgemaakte Fouten
Fout — store importen zonder aanroepen:
import { useCounterStore } from '@/stores/counter'
console.log(useCounterStore.count) // ❌ useCounterStore is een functie
Goed — eerst aanroepen:
const store = useCounterStore() // ✅
console.log(store.count)
Fout — store destructuren (verbreekt reactivity):
const { count } = useCounterStore()
// ❌ count is geen ref meer, updates komen niet door
Goed — gebruik storeToRefs of de store direct:
// Optie 1: store als geheel
const store = useCounterStore()
console.log(store.count) // ✅
// Optie 2: storeToRefs voor destructuring
import { storeToRefs } from 'pinia'
const { count } = storeToRefs(useCounterStore()) // ✅