404 Not Found

404 Not Found


nginx

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


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:

TYPESCRIPT
// 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

BASH
npm install @pinia/nuxt pinia

(1) ▶Exemplo: Configuração do nuxt.config.ts

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  // Pinia é auto-configurado, sem necessidade de setup extra
})

Saída:

TEXT
// 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

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(2) Store com Sintaxe Setup

(2) ▶Exemplo: Store de Usuário com Sintaxe Setup

TYPESCRIPT
// 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:

TEXT
// 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

100%
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

TYPESCRIPT
// 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:

TEXT
// 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

VUE
<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:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: Atualização em lote com $patch

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

(3) ▶Exemplo: Plugin de Persistência do Carrinho de Compras

TYPESCRIPT
// 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:

TEXT
// Execução Bem-sucedida

7. Exemplo Completo: Sistema de Carrinho de Compras da MegaShop

VUE
<!-- 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

P Como escolher entre Pinia Store e Composable?
R Use Store quando precisar compartilhar estado entre componentes, e use Composable para reutilizar lógica dentro de um componente. Use Store para carrinho de compras e estado do usuário, e use Composable para formatação de preços.
P Por que desestruturar um Store faz com que ele perca sua reatividade?
R Desestruturar diretamente um Store retorna uma cópia do valor, não um ref. Use storeToRefs para desestruturar mantendo a reatividade; actions podem ser desestruturadas diretamente (funções não requerem reatividade).
P O que fazer se houver inconsistências nos dados do Store entre cliente e servidor durante o SSR?
R Certifique-se de não modificar o Store antes do 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.
P O $reset do Pinia não está disponível na sintaxe Setup?
R A sintaxe Setup não inclui $reset. Você pode implementar isso manualmente definindo uma action $reset ou resetando todos os refs em $patch.
P Modificar o Store em múltiplas páginas simultaneamente causará conflitos?
R O cliente é single-threaded, então não haverá conflitos. Durante o SSR, cada requisição tem sua própria instância independente do Pinia, então também não haverá conflitos.
P Como reter os dados do carrinho de compras após atualizar a página?
R Use um plugin de persistência (armazenando dados no localStorage) ou sincronize com um cookie. A MegaShop recomenda usar cookies para sincronização, pois eles podem ser lidos mesmo durante a renderização no lado do servidor (SSR).

📖Resumo


📝Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Crie useCartStore, implemente addItem e removeItem, manipule o carrinho de compras em duas páginas diferentes e verifique se o estado é compartilhado.
  2. Exercício Avançado (Dificuldade: ⭐⭐): Use a sintaxe Setup para definir useUserStore, implemente login, logout e fetchProfile, e verifique se a hidratação SSR funciona corretamente.
  3. 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.

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%