Gerenciamento de Estado Pinia
Após Alice adicionar um item ao carrinho, o carrinho fica vazio quando ela navega para outra página—cada página tem seu próprio estado de componente. Bob nota que o status de login do usuário é inconsistente entre o SSR e o lado do cliente. Charlie precisa do Pinia para gerenciar o estado compartilhado entre componentes e páginas.
1. O Que Você Vai Aprender
- Instalação e Configuração Automática do Módulo @pinia/nuxt
- Definição de Store: Sintaxe Option-Based e Setup
- Mecanismos de Desidratação e Hidratação no Estado SSR
- Desestruturação Reativa com storeToRefs e $patch/$reset
- Carrinho de Compras MegaShop: Adicionar, Remover, Atualizar e Consultar + Cálculo de Preço Total
2. Uma História Real de uma Consumidora
(1) Dor: Status do Carrinho de Compras "Perdido"
Alice adicionou três itens ao carrinho na homepage da MegaShop, mas quando clicou para ver a página de detalhes do produto, o ícone do carrinho mostrava 0 itens—o status não estava sincronizado. Bob encontrou um problema semelhante: após fazer login e atualizar a página, seu status de logado desapareceu.
(2) Solução com Gerenciamento de Estado Pinia
O Pinia desacopla o carrinho de compras e o estado do usuário do ciclo de vida do componente, transformando-os em dados globalmente compartilhados e reativos:
// composables/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
return { items, total }
})
(3) Benefício: Persistência de estado entre páginas
O carrinho de compras da Alice permanece consistente em todas as páginas, e o status de login do Bob agora está sincronizado entre o SSR e o cliente, eliminando problemas de perda de estado.
3. Instalação e Configuração do Pinia
(1) Passos de Instalação
npm install @pinia/nuxt pinia
(1) ▶Exemplo: Configuração do nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
// Pinia é auto-configurado, sem necessidade de setup extra
})
Saída:
// Execução Bem-sucedida
(2) Comparação de Pinia, Vuex e useState
| Dimensão | Pinia | Vuex 4 | Nuxt useState |
|---|---|---|---|
| Suporte Vue 3 | ✅ Nativo | ⚠️ Modo Compatibilidade | ✅ Exclusivo Nuxt |
| TypeScript | ✅ Inferência completa de tipos | ❌ Declaração manual necessária | ✅ Suporte a Genéricos |
| Hidratação SSR | ✅ Automática | ⚠️ Requer configuração | ✅ Automática |
| Code Splitting | ✅ Sob demanda | ❌ Global | ❌ Global |
| DevTools | ✅ Suportado | ✅ Suportado | ❌ Não suportado |
| Tamanho | ~1 KB | ~6 KB | Integrado |
| Casos de Uso | Cenários Complexos | Migração Legada | Compartilhamento Simples |
4. Como Definir um Store
(1) Store Baseado em Opções
(1) ▶Exemplo: Store de Carrinho de Compras Baseado em Opções
// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
couponCode: '' as string
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
formattedTotal(): string {
return new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(this.totalPrice)
}
},
actions: {
addItem(product: Product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
},
removeItem(productId: number) {
this.items = this.items.filter(i => i.id !== productId)
},
updateQuantity(productId: number, quantity: number) {
const item = this.items.find(i => i.id === productId)
if (item) item.quantity = Math.max(0, quantity)
this.items = this.items.filter(i => i.quantity > 0)
},
clearCart() {
this.items = []
this.couponCode = ''
}
}
})
interface CartItem {
id: number; name: string; price: number; quantity: number; image: string
}
interface Product {
id: number; name: string; price: number; image: string
}
Saída:
// Execução Bem-sucedida
(2) Store com Sintaxe Setup
(2) ▶Exemplo: Store de Usuário com Sintaxe Setup
// stores/user.ts
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!user.value)
const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')
// Actions
async function login(email: string, password: string) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
function logout() {
user.value = null
}
async function fetchProfile() {
const profile = await $fetch('/api/user/profile')
user.value = profile
}
return {
user, isAuthenticated, fullName,
login, logout, fetchProfile
}
})
interface User {
id: number; email: string; firstName: string; lastName: string; role: 'customer' | 'admin'
}
Saída:
// Execução Bem-sucedida
(3) Comparação dos Dois Métodos
| Dimensão | Tipo Opção | Sintaxe Setup |
|---|---|---|
| Sintaxe | state/getters/actions | ref/computed/function |
| TypeScript | ⚠️ Declaração de interface necessária | ✅ Inferência automática |
| Flexibilidade | ⚠️ Limitada | ✅ Disponível em qualquer Composable |
| SSR | ✅ Suporta $reset | ⚠️ Não suporta $reset (deve ser implementado manualmente) |
| Casos de Uso | Store Simples | Store Complexo/Requer Composable |
5. Desidratação e Hidratação SSR
(1) Transições de Estado SSR do Pinia
flowchart LR
A[Servidor: Store preenchido com dados] --> B[Serializar estado para payload HTML]
B --> C[Cliente: Ler payload]
C --> D[Hidratar Store com estado do servidor]
D --> E[Cliente: Store pronto, sem re-fetch]
(1) ▶Exemplo: Inicializando o Store no SSR
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
const cartStore = useCartStore()
// Inicializar carrinho a partir do cookie no servidor
const cartCookie = useCookie('cart-items')
if (cartCookie.value) {
cartStore.items = cartCookie.value
}
})
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Diferenças Entre Pinia e useState no SSR
| Dimensão | Pinia | useState |
|---|---|---|
| Hidratação SSR | ✅ Payload automático | ✅ Payload automático |
| Multi-Store | ✅ Namespaces Independentes | ⚠️ Gerenciamento Manual de Keys |
| DevTools | ✅ Visualização | ❌ Nenhum |
| Persistência | ✅ Suporte a Plugins | ⚠️ Cookie manual necessário |
| Lógica complexa | ✅ actions/getters | ❌ Apenas ref |
6. storeToRefs e Operações em Lote
(1) ▶Exemplo: Desestruturação Reativa com storeToRefs
<script setup lang="ts">
const cartStore = useCartStore()
// ✅ Desestruturação reativa - mantém reatividade
const { items, totalPrice, totalItems } = storeToRefs(cartStore)
// ❌ Desestruturação direta - perde reatividade
// const { items, totalPrice } = cartStore
// Actions podem ser desestruturadas diretamente (sem necessidade de reatividade)
const { addItem, removeItem, clearCart } = cartStore
</script>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Atualização em lote com $patch
// Atualização em lote com $patch - único gatilho de reatividade
const cartStore = useCartStore()
// Estilo objeto
cartStore.$patch({
couponCode: 'SAVE20',
items: [...cartStore.items, newItem]
})
// Estilo função (melhor para mutações em arrays)
cartStore.$patch((state) => {
state.couponCode = 'SAVE20'
state.items.push(newItem)
state.items[0].quantity = 3
})
Saída:
// Execução Bem-sucedida
(3) ▶Exemplo: Plugin de Persistência do Carrinho de Compras
// plugins/pinia-persist.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(({ store }) => {
// Carregar do localStorage no cliente
const saved = localStorage.getItem(`pinia-${store.$id}`)
if (saved) store.$patch(JSON.parse(saved))
// Salvar no localStorage ao mudar
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
})
})
Saída:
// Execução Bem-sucedida
7. Exemplo Completo: Sistema de Carrinho de Compras da MegaShop
<!-- pages/cart.vue -->
<template>
<div class="cart-page">
<h1>Carrinho de Compras</h1>
<div v-if="items.length === 0" class="empty-cart">
<p>Seu carrinho está vazio</p>
<NuxtLink to="/products">Continuar Comprando</NuxtLink>
</div>
<div v-else>
<div class="cart-items">
<div v-for="item in items" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" />
<div class="details">
<h3>{{ item.name }}</h3>
<p>${{ item.price }} USD</p>
<div class="quantity">
<button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
</div>
<button @click="removeItem(item.id)" class="remove">Remover</button>
</div>
</div>
<div class="cart-summary">
<p>Itens: {{ totalItems }}</p>
<p class="total">Total: {{ formattedTotal }}</p>
<button @click="clearCart" class="clear">Limpar Carrinho</button>
<NuxtLink to="/checkout" class="checkout">Ir para Checkout</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, formattedTotal } = storeToRefs(cartStore)
const { removeItem, updateQuantity, clearCart } = cartStore
</script>
❓Perguntas Frequentes
storeToRefs para desestruturar mantendo a reatividade; actions podem ser desestruturadas diretamente (funções não requerem reatividade).onMounted (valores modificados durante a fase de SSR serão sobrescritos durante a hidratação). A hidratação SSR do Pinia é automática e tipicamente não requer tratamento manual.$reset do Pinia não está disponível na sintaxe Setup?$reset. Você pode implementar isso manualmente definindo uma action $reset ou resetando todos os refs em $patch.📖Resumo
- Pinia é a solução oficial de gerenciamento de estado para Vue 3; o módulo @pinia/nuxt fornece integração automática
- O Store baseado em opções tem uma estrutura clara (state/getters/actions), e a sintaxe Setup é mais flexível
- Hidratação SSR Auto-Completa: Dados do Store no servidor → payload → Store no cliente
storeToRefsmantém a desestruturação reativa;$patchaciona apenas uma renderização mesmo com atualizações em lote- MegaShop usa Pinia para gerenciar carrinhos de compras e status de usuários, e depende de cookies para persistência
📝Exercícios
- Exercício Básico (Dificuldade: ⭐): Crie
useCartStore, implementeaddItemeremoveItem, manipule o carrinho de compras em duas páginas diferentes e verifique se o estado é compartilhado. - Exercício Avançado (Dificuldade: ⭐⭐): Use a sintaxe Setup para definir
useUserStore, implementelogin,logoutefetchProfile, e verifique se a hidratação SSR funciona corretamente. - Desafio (Dificuldade: ⭐⭐⭐): Implemente persistência do carrinho de compras (para que os dados sejam retidos após atualizar a página), e compare os prós e contras de usar o plugin localStorage versus cookies para sincronização.
---|



