Gerenciamento de Estado em 2026: Boas Práticas com Pinia cover image

Gerenciamento de Estado em 2026: Boas Práticas com Pinia

O gerenciamento de estado deixou de ser um debate filosófico no ecossistema Vue há alguns anos. Com Vue 3, a Composition API e o Nuxt como framework de primeira classe, o Pinia é o padrão prático em 2026. Ele é leve, amigável ao TypeScript e construído em cima do mesmo modelo mental que você já usa nos componentes.

Isso não significa que todo dado deve ir para uma store. As melhores configurações de Pinia são previsíveis: estrutura de pastas clara, limites bem definidos entre estado de servidor e cliente, e stores que fazem uma coisa só—e fazem bem. Este post reúne os padrões que uso em apps Nuxt e Vue em produção: não são dicas genéricas, mas hábitos que continuam funcionando quando o código cresce.

Comece pela pergunta certa: isso precisa de uma store?

Antes de criar useCartStore(), pergunte se o estado é realmente compartilhado. Estado local de UI—visibilidade de modal, rascunho de formulário, accordion aberto/fechado—geralmente fica no componente ou em um composable pequeno. O Pinia brilha quando várias rotas, layouts ou componentes aninhados precisam da mesma fonte da verdade.

Uma regra útil em 2026: comece com composables e promova para Pinia quando o custo de coordenação aumentar. Se dois componentes irmãos precisam da mesma lista buscada e você está fazendo prop drilling ou duplicando fetch, uma store (ou um composable apoiado por store) faz sentido. Se só uma página se importa, mantenha local.

  • Compartilhado entre rotas: sessão de auth, carrinho, feature flags, workspace ativo

  • Dados de servidor com mutações no cliente: listas de tarefas, inbox de notificações, rascunhos de posts

  • Orquestração de UI transversal: fila global de toasts, command palette, wizard multi-etapas

  • Manter local: estados de hover, validação de um formulário, filtros pontuais em uma tabela

Organize stores por domínio, não por tipo de arquivo

Um erro comum é espelhar endpoints REST um a um—useUsersStore, useUserRolesStore, useUserPreferencesStore—até que toda action exija importar três stores. Em vez disso, agrupe por capacidade de negócio.

Em um app Nuxt típico, estruturo stores assim:

  • stores/auth.ts — sessão, permissões, login/logout

  • stores/cart.ts — itens, totais, intenção de checkout

  • stores/workspace.ts — org ativa, membros, contexto de billing

Pastas aninhadas funcionam quando um domínio cresce (stores/billing/invoices.ts), mas evite dividir cedo demais. Uma store com 200 linhas e actions coesas é mais saudável do que cinco stores que se chamam em círculo.

No Nuxt, mantenha stores no diretório stores/ para o auto-import funcionar. Nomeie exports de forma consistente: export const useAuthStore = defineStore('auth', ...). O id da store deve ser estável—ele faz parte da identidade no persist e no DevTools.

Prefira a sintaxe Setup Store

O Pinia suporta sintaxe Options e Setup. Em 2026, setup stores são o padrão melhor para projetos TypeScript porque seguem os mesmos padrões da Composition API que você já usa em <script setup>.

// stores/cart.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
  )

  function addItem(product: Product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) existing.qty += 1
    else items.value.push({ ...product, qty: 1 })
  }

  return { items, total, addItem }
})

Setup stores facilitam extrair lógica para composables. Paginação ou busca com debounce podem viver em usePaginatedFetch() enquanto a store conecta os resultados ao estado compartilhado. Essa separação mantém stores legíveis e simplifica testes unitários.

Mantenha actions assíncronas; mantenha getters puros

Actions do Pinia podem ser async—trate-as como camada de mutação e efeitos colaterais. Busque dados, chame APIs, atualize vários refs e retorne um resultado que os componentes consigam reagir. Getters (ou computed em setup stores) devem derivar estado sem efeitos colaterais.

Um padrão que escala bem:

  • Actions orquestram: await login(credentials), await refreshSession()

  • Getters/computed derivam: isAuthenticated, canEditProject, sortedItems

  • Componentes chamam actions e leem estado—evite mutar refs da store direto no template quando uma action expressa melhor a intenção

Quando uma action falha, retorne erros estruturados ou lance exceções de forma consistente. Espalhar try/catch em todo componente gera ruído. Centralizar tratamento de erro na store—ou em um client de API compartilhado—mantém a UX previsível.

TypeScript: deixe a inferência trabalhar

Uma força do Pinia é que setup stores inferem tipos de retorno automaticamente. Raramente você precisa de tipagem manual pesada, a menos que esteja publicando uma biblioteca.

Ainda assim, alguns hábitos com TypeScript valem a pena:

  • Defina tipos de domínio em types/ ou módulos colocados—não dentro de componentes

  • Use storeToRefs ao desestruturar em componentes para preservar reatividade

  • Recorra a ReturnType<typeof useAuthStore> só ao integrar código fora do Vue; dentro do Vue, use a store diretamente

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '~/stores/cart'

const cart = useCartStore()
const { items, total } = storeToRefs(cart)
const { addItem } = cart
</script>

Evite desestruturar state direto da store sem storeToRefs. Esse ainda é um dos bugs de reatividade mais comuns que vejo em code reviews.

Nuxt e SSR: trate estado de servidor e cliente de forma diferente

Em apps Nuxt 3/4, nem todo estado Pinia deve sobreviver à passagem servidor-cliente. Tokens sensíveis, listas transientes grandes ou dados que precisam estar frescos a cada request exigem cuidado.

Diretrizes práticas de SSR:

  • Hidrate com intenção. Use useAsyncData ou server routes para dados iniciais e só então alimente a store em plugin ou setup de página quando ela realmente precisar ser compartilhada no cliente

  • Evite persistir segredos. Não guarde JWTs crus em estado Pinia persistido no cliente sem entender o modelo de ameaça

  • Reset no logout. Chame $reset() em options stores ou limpe refs manualmente em setup stores ao encerrar sessões

  • Observe fetches duplicados. Se a página busca em useAsyncData e a store busca de novo no mount, você paga duas vezes

Em dashboards autenticados, costumo manter dados buscados no servidor na página e reservar Pinia para contexto de sessão entre páginas e mutações no cliente. Essa divisão reduz surpresas de hidratação.

Persistência: use com moderação

pinia-plugin-persistedstate é popular por um motivo, mas persistência é fácil de exagerar. Persista preferências do usuário, rascunhos de carrinho e configurações de UI. Não persista respostas inteiras de API que podem ficar stale ou vazar entre usuários em dispositivos compartilhados.

Quando persistir:

  • Escopo de chaves por usuário ou workspace

  • Versione o payload persistido e migre em breaking changes

  • Exclua campos voláteis explicitamente

Performance sem otimização prematura

O Pinia já é eficiente para a maioria dos apps. Otimize quando a medição pedir, não quando o medo pedir.

  • Divida stores antes que uma store vire um módulo quente re-renderizado em todo lugar

  • Prefira selectors computed em vez de campos derivados duplicados que você precisa sincronizar manualmente

  • Use shallow refs para coleções grandes quando reatividade profunda não for necessária

  • No Pinia 2.2+, explore padrões de subscribeOnScope em composables ligados ao ciclo de vida do componente

Em componentes, não mapeie objetos inteiros da store no template se você só precisa de dois campos. Assinaturas estreitas mantêm renders baratos.

Testar stores é simples—faça

Setup stores são funções. Teste como funções: crie uma instância Pinia nova, monte a store, chame actions, asserte o estado.

import { createPinia, setActivePinia } from 'pinia'
import { useCartStore } from '../cart'

beforeEach(() => {
  setActivePinia(createPinia())
})

it('adiciona itens e calcula total', () => {
  const cart = useCartStore()
  cart.addItem({ id: '1', price: 10, name: 'Widget' })
  expect(cart.total).toBe(10)
})

Mock chamadas de rede na camada de API, não dentro de cada teste de store. Stores devem ser finas o suficiente para testes rápidos e legíveis.

DevTools e convenções de time

O Pinia DevTools continua essencial para debugar cadeias de actions e reproduzir bugs localmente com time-travel. Convenções de time importam tanto quanto ferramentas:

  • Actions nomeadas como verbos: fetchProjects, updateProfile, clearFilters

  • Nenhuma regra de negócio escondida em componentes que deveria estar numa action da store

  • Documente quais stores são persistidas e quais são efêmeras

  • Mantenha ids de store estáveis entre releases

Armadilhas comuns em 2026

  • A mega-store: um arquivo com auth, UI e dados de domínio—divida

  • Store como service locator: importar stores dentro de stores não relacionadas cria acoplamento oculto; prefira composables ou parâmetros explícitos

  • Duplicar cache de servidor: se você adota TanStack Query ou a camada async do Nuxt, deixe que ela cuide de fetch/cache; use Pinia para intenção compartilhada no cliente, não um segundo cache

  • Mutar props via stores: stores substituem prop drilling—não corrigem problemas de ownership de dados upstream

Considerações finais

O Pinia venceu porque parece Vue, não porque ganhou um benchmark. As melhores práticas em 2026 têm menos a ver com decorar APIs e mais com traçar limites claros: local vs compartilhado, servidor vs cliente, derivado vs fonte da verdade.

Comece pequeno, organize por domínio, use setup stores com bons hábitos de TypeScript e integre com o modelo SSR do Nuxt de forma consciente. Quando o app superar um padrão, divida stores ou extraia composables—o Pinia dá margem para evoluir sem reescrita.

Esse é o objetivo: gerenciamento de estado que sai do caminho enquanto a complexidade do produto cresce.