
Pinia to rekomendowana biblioteka do zarządzania stanem w Vue 3, Vuex jest w trybie utrzymania. Store deklaruje się przez defineStore, używa przez wywołanie usePanierStore() w dowolnym komponencie i destrukturyzuje przez storeToRefs, żeby nie stracić reaktywności.
Przekazanie zdarzenia z komponentu do rodzica sprawdza się na jednym, może dwóch poziomach. Wyżej spędzasz czas na przekazywaniu zdarzeń dalej i przepychaniu propsów przez komponenty, którym są zupełnie niepotrzebne. Pinia rozwiązuje ten problem: stan deklarujesz raz, a czytasz go i zmieniasz z dowolnego komponentu.
Pinia czy Vuex?
To pytanie już nie istnieje. Vuex jest w trybie utrzymania, a oficjalna dokumentacja Vue odsyła do Pinii jako rekomendowanej biblioteki do zarządzania stanem. W nowym projekcie bierz Pinię bez wahania. W działającym projekcie na Vuexie migracja nie jest pilna: robi się ją store po storze, obie biblioteki spokojnie współistnieją.
Co Pinia zmienia w praktyce: żadnych mutacji, tylko akcje. żadnych zagnieżdżonych modułów, bo każdy store jest modułem, i typowanie, które działa bez akrobacji.
Instalacja
npm install piniaNastępnie podepnij plugin do aplikacji:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')W nowym projekcie robi to za ciebie oficjalny generator: odpowiedz „tak” na pytanie o Pinię podczas npm create vue@latest.
Pisanie store’a
Store deklaruje się przez defineStore. Pierwszy argument to unikalny identyfikator, używany przez narzędzia deweloperskie. Drugi może przyjąć dwie formy, forma setup jest tą, którą generuje oficjalne narzędzie, i tą, która najbardziej przypomina komponent.
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const usePanierStore = defineStore('panier', () => {
// stan: ref-y
const lignes = ref([])
// gettery: 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)
)
// akcje: funkcje
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 }
})Wszystko, co funkcja zwróci, staje się publiczne. To, czego nie zwróci, zostaje prywatne dla store’a: tak trzyma się w nim wewnętrzny licznik albo funkcję pomocniczą.
Konwencja nazewnicza to useQuelqueChoseStore, dokładnie jak w composable.
Forma options
Drugi zapis powtarza strukturę Vuexa, tyle że bez mutacji:
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 })
}
}
})Obie formy są równoważne i wymienne. Forma options ułatwia przesiadkę z Vuexa, forma setup daje więcej swobody, choćby w użyciu composable wewnątrz store’a.
Używanie store’a
Wywołaj funkcję w komponencie. Instancja jest jedna: dwa komponenty wywołujące usePanierStore() dostają dokładnie ten sam obiekt.
<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>Stan, gettery i akcje są dostępne bezpośrednio na obiekcie, bez żadnego pośrednika.
storeToRefs, czyli utracona reaktywność
To najczęstszy błąd przy Pinii. Destrukturyzacja store’a daje zamrożone wartości:
const panier = usePanierStore()
const { total } = panier // ✗ zamrożona liczba, już się nie zmieni
panier.ajouter({ id: 1, prix: 7 })
console.log(total) // 0storeToRefs wyciąga stan i gettery, zachowując reaktywność:
import { storeToRefs } from 'pinia'
const panier = usePanierStore()
const { total, lignes } = storeToRefs(panier) // ✓ ref-y
const { ajouter, retirer } = panier // ✓ akcje destrukturyzuje się wprost
panier.ajouter({ id: 1, prix: 7 })
console.log(total.value) // 7Zapamiętaj zasadę: stan i gettery przechodzą przez storeToRefs, akcje destrukturyzuje się wprost. Akcje to zwykłe funkcje związane ze storem, nic nie tracą.
Zmiana stanu
Trzy sposoby, w kolejności od najlepszego.
Akcja do każdej logiki biznesowej. To przypadek normalny: reguła jest zapisana raz, w storze.
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })$patch do zmiany kilku wpisów w jednej operacji, co daje tylko jeden wpis w narzędziach deweloperskich:
panier.$patch({ lignes: [], codePromo: null })
// forma funkcyjna, dla tablic
panier.$patch((state) => {
state.lignes.push(nouvelleLigne)
state.derniereMaj = Date.now()
})Bezpośrednie przypisanie jest dozwolone, inaczej niż w Vuexie, ale zostaw je do przypadków trywialnych:
panier.codePromo = 'RENTREE'W storze napisanym w formie options panier.$reset() przywraca stan początkowy. W storze w formie setup Pinia tego stanu początkowego nie zna: wywołanie rzuca błąd. Napisz własną akcję czyszczącą, taką jak funkcja vider() powyżej.
Obserwowanie zmian
$subscribe jest wywoływane przy każdej zmianie stanu. To haczyk, na którym wiesza się utrwalanie koszyka:
panier.$subscribe((mutation, state) => {
localStorage.setItem('panier', JSON.stringify(state.lignes))
})Przy szerszej potrzebie utrwalania robotę na wszystkich store’ach wykona plugin Pinii. Plugin to funkcja, która dostaje kontekst i wzbogaca każdy store:
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)Testowanie store’a
Store testuje się bez montowania komponentu. Wystarczy przed każdym testem aktywować świeżą instancję Pinii, żeby startować z czystego stanu:
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)
})
})Bez setActivePinia wywołanie store’a kończy się komunikatem „”getActivePinia()” was called but there was no active Pinia. Are you trying to use a store before calling „app.use(pinia)”?”.
Kiedy nie sięgać po Pinię
Store nie jest darmowy: dokłada plik, warstwę pośrednią i kolejny globalny stan. Ma sens wtedy, gdy dane są używane przez odległe od siebie komponenty albo gdy mają przetrwać odmontowanie komponentu.
Przy danych z usługi czasu rzeczywistego zerknij raczej na Firebase i VueFire, których wiązania są już reaktywne.
Do lokalnego stanu formularza wystarczy ref. Do stanu dzielonego między rodzicem a jego bezpośrednim dzieckiem robotę wykonają propsy i zdarzenia. Do cache’owania danych z serwera lepiej odpowie dedykowane narzędzie w rodzaju TanStack Query.
Jeśli dopiero poznajesz Vue, zacznij od kursu Vue 3 dla początkujących: Pinia nabiera sensu, kiedy masz już za sobą komponenty i zdarzenia.
Częste błędy
const { total } = panier daje zamrożoną wartość. Stan i gettery przepuść przez storeToRefs(panier), akcje destrukturyzuje się wprost.n$reset w storze setup | Pinia nie zna stanu początkowego store'a napisanego w formie setup: wywołanie rzuca błąd. Napisz własną akcję czyszczącą.setActivePinia(createPinia()) przed każdym przypadkiem.

