O que é Pinia?
Pinia é a biblioteca oficial de gerenciamento de estado para Vue. Ela substituiu o Vuex como a store recomendada no Vue 3 e agora é o padrão em novos projetos Vue e Nuxt. É leve, type-safe e construída diretamente sobre a Composition API.
O design é intencionalmente minimalista. Uma store é uma função que retorna state, valores derivados e actions. Não existem mutations, nem namespacing de módulos ou providers por store. Se você conhece ref, computed e funções, você já conhece a maior parte do Pinia.
Definindo uma store
Uma store é criada com defineStore, que recebe um id único e uma função de setup.
// 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 };
});
Esta é uma setup store: ref torna-se o state, computed torna-se um getter e as funções tornam-se actions. O id único nomeia a store no devtools e é obrigatório.
O Pinia também suporta uma options store, caso você prefira a estrutura semelhante ao Vuex:
// stores/counter.js
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
});
Ambas as formas são totalmente suportadas. Setup stores tendem a se encaixar melhor com TypeScript e a Composition API, enquanto options stores podem parecer mais familiares para usuários de Vuex.
Usando uma store em componentes
Chame a função da store para obter a instância da store. Não há necessidade de um provider para envolver seus componentes.
<!-- 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>
A leitura de cart.total diretamente em um template permanece reativa. Quando você quiser fazer o destructuring do estado ou dos getters mantendo a reatividade, use storeToRefs, que os converte em refs. As actions podem sofrer destructuring diretamente, pois não precisam de reatividade.
Getters
Getters são valores computados derivados do estado. Eles são cacheados e compartilhados, portanto, todo componente que lê o mesmo getter compartilha o mesmo cálculo.
// 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 };
});
Tentar usar watch para computar um valor é um erro comum. Se o valor puder ser derivado do estado existente, ele deve ser um getter, exatamente da mesma forma que você usaria computed em um componente.
Actions
Actions são funções que alteram o estado. Elas podem ser síncronas ou assíncronas, e podem chamar outras actions ou até mesmo outras stores.
// 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 };
});
Para dados do servidor, no entanto, considere uma camada de busca de dados como o TanStack Query, que gerencia cache e invalidação. O Pinia é ideal para estado do cliente: carrinhos, filtros, preferências de UI, estado de autenticação e rascunhos.
Plugins e persistência
Os plugins do Pinia são executados para cada store e podem adicionar comportamentos como persistência, logging ou reset.
// 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);
O popular pinia-plugin-persistedstate faz a mesma coisa, oferecendo opções sobre quais chaves devem ser armazenadas. A persistência é um dos principais motivos para se utilizar uma store em vez do estado local de um componente.
Pinia com Nuxt
No Nuxt, instale o módulo oficial e as stores em stores/ serão importadas automaticamente.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>
Como o Nuxt gerencia a instalação e a renderização no servidor, a mesma store funciona tanto no servidor quanto no cliente. Consulte o guia do Nuxt para entender como isso se integra ao useFetch e às rotas de servidor.
Melhores práticas
- Prefira setup stores para códigos novos, especialmente com TypeScript.
- Mantenha dados derivados em getters em vez de observar o estado.
- Use
storeToRefsao desestruturar o estado ou getters. - Mantenha dados do servidor em uma camada de data-fetching, não no Pinia.
- Use actions para qualquer coisa que altere o estado, incluindo operações assíncronas.
- Persista apenas o estado que deve sobreviver a um reload.
- Nomeie as stores de forma clara, com base no domínio que elas gerenciam.
Erros comuns
- Desestruturar o state sem
storeToRefse perder a reatividade. - Usar
watchpara calcular um valor que deveria ser derivado por um getter. - Colocar dados buscados do servidor no store e gerenciar manualmente as flags de carregamento.
- Esquecer de instalar o Pinia, fazendo com que as chamadas do store falhem em runtime.
- Criar um único store enorme em vez de stores focados em domínios específicos.
- Mutar o state fora de uma action de uma forma que seja difícil de rastrear.
Próximos passos
O Pinia é o equivalente do Vue para um store pequeno e moderno. Aprofunde seus conhecimentos em Vue, adicione Nuxt para renderização no servidor e auto-imports, e compare essa abordagem com a Context API, Zustand e Redux Toolkit do React. Depois, crie um store pequeno com um getter, uma action assíncrona e persistência para ver a pouca quantidade de código necessária.