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:

  1. Bestandsnaam = useXxxStore
  2. Eerste argument van defineStore is 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 defineStore is een unieke ID-string
  • Roep useXxxStore() aan in <script setup>, niet op top-level van JS-bestanden
  • Activeer Pinia in main.js met app.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())  // ✅