
O Pinia é a biblioteca de gestão de estado recomendada para o Vue 3, o Vuex está em manutenção. Um store declara-se com defineStore, usa-se chamando usePanierStore() em qualquer componente, e desestrutura-se com storeToRefs para não perder a reatividade.
Fazer subir um evento de um componente para o seu pai funciona bem em um ou dois níveis. Para lá disso, passas a vida a reemitir eventos e a fazer descer props por componentes que não fazem nada com elas. O Pinia resolve o problema: um estado declarado uma vez, legível e alterável a partir de qualquer componente.
Pinia ou Vuex?
A pergunta já não se coloca. O Vuex está em manutenção e a documentação oficial do Vue remete para o Pinia, que passou a ser a biblioteca de gestão de estado recomendada. Num projeto novo, escolhe o Pinia sem hesitar. Num projeto em Vuex que funciona, a migração não tem urgência nenhuma: faz-se store a store, os dois convivem.
O que o Pinia muda na prática: acabaram-se as mutations, só há actions; acabaram-se os módulos aninhados, cada store é um módulo, e a tipagem funciona sem contorcionismos.
Instalar
npm install piniaDepois liga o plugin à aplicação:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')Num projeto novo, o gerador oficial trata disso: responde «sim» ao Pinia durante o npm create vue@latest.
Escrever um store
Um store declara-se com defineStore. O primeiro argumento é um identificador único, usado pelas ferramentas de desenvolvimento. O segundo pode assumir duas formas, a forma setup é a que as ferramentas oficiais geram e a que mais se parece com um componente.
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const usePanierStore = defineStore('panier', () => {
// estado: refs
const lignes = ref([])
// getters: 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: funções
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 }
})Tudo o que a função devolve passa a ser público. O que ela não devolve fica privado ao store: é assim que lá guardas um contador interno ou uma função utilitária.
A convenção de nomes é useQuelqueChoseStore, como num composable.
A forma options
A outra escrita retoma a estrutura do Vuex, sem as 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 })
}
}
})As duas formas são equivalentes e permutáveis. A forma options facilita a chegada a partir do Vuex, a forma setup dá mais liberdade, sobretudo para usar um composable dentro do store.
Usar o store
Chama a função no componente. A instância é única: dois componentes que chamem usePanierStore() obtêm exatamente o mesmo objeto.
<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>Estado, getters e actions alcançam-se diretamente no objeto, sem passar por nenhum intermediário.
storeToRefs, ou a reatividade perdida
É o erro mais frequente com o Pinia. Desestruturar o store dá valores congelados:
const panier = usePanierStore()
const { total } = panier // ✗ número congelado, não voltará a mudar
panier.ajouter({ id: 1, prix: 7 })
console.log(total) // 0O storeToRefs extrai o estado e os getters mantendo a reatividade:
import { storeToRefs } from 'pinia'
const panier = usePanierStore()
const { total, lignes } = storeToRefs(panier) // ✓ refs
const { ajouter, retirer } = panier // ✓ as actions desestruturam-se
panier.ajouter({ id: 1, prix: 7 })
console.log(total.value) // 7Guarda a regra: o estado e os getters passam pelo storeToRefs, as actions desestruturam-se diretamente. As actions são simples funções ligadas ao store, não perdem nada.
Alterar o estado
Três maneiras, por ordem de preferência.
Uma action para toda a lógica de negócio. É o caso normal: a regra fica escrita uma vez, dentro do store.
panier.ajouter({ id: 2234, nom: 'T-Shirt', prix: 25 })O $patch para alterar várias entradas numa só operação, o que produz uma única entrada nas ferramentas de desenvolvimento:
panier.$patch({ lignes: [], codePromo: null })
// forma função, para os arrays
panier.$patch((state) => {
state.lignes.push(nouvelleLigne)
state.derniereMaj = Date.now()
})A atribuição direta é permitida, ao contrário do Vuex, mas reserva-a para os casos triviais:
panier.codePromo = 'RENTREE'Num store escrito na forma options, o panier.$reset() repõe o estado inicial. Num store na forma setup, o Pinia não conhece esse estado inicial: a chamada lança um erro. Escreve a tua própria action de reposição, como a função vider() acima.
Observar as alterações
O $subscribe é chamado a cada mutação do estado. É o gancho certo para persistir o carrinho:
panier.$subscribe((mutation, state) => {
localStorage.setItem('panier', JSON.stringify(state.lignes))
})Para uma necessidade de persistência mais ampla, um plugin Pinia faz o trabalho em todos os stores. Um plugin é uma função que recebe o contexto e enriquece cada 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)Testar um store
Um store testa-se sem montar componente nenhum. Basta ativar uma instância Pinia nova antes de cada teste, para partir de um estado limpo:
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)
})
})Sem setActivePinia, a chamada ao store falha com a mensagem « “getActivePinia()” was called but there was no active Pinia. Are you trying to use a store before calling “app.use(pinia)”? ».
Quando não usar o Pinia
Um store não é gratuito: acrescenta um ficheiro, uma indireção e mais um estado global. Justifica-se quando um dado é usado por componentes distantes, ou quando tem de sobreviver à desmontagem de um componente.
Para dados que vêm de um serviço em tempo real, vê antes o Firebase e o VueFire, cujas ligações já são reativas.
Para o estado local de um formulário, um ref chega. Para um estado partilhado entre um pai e o seu filho direto, as props e os eventos fazem o trabalho. Para cache de dados do servidor, uma ferramenta dedicada como o TanStack Query responde melhor à necessidade.
Se estás a descobrir o Vue agora, começa pelo tutorial de Vue 3 para principiantes: o Pinia só ganha sentido depois de dominares os componentes e os eventos.
Erros frequentes
const { total } = panier dá um valor congelado. Passa por storeToRefs(panier) para o estado e os getters, as actions, essas, desestruturam-se diretamente.n$reset num store setup | O Pinia não conhece o estado inicial de um store escrito na forma setup: a chamada lança um erro. Escreve a tua própria action de reposição.setActivePinia(createPinia()) antes de cada caso.

