💻 Exemplos: Capítulo 13: Gerenciamento de Estado Global Oficial com Pinia (defineStore)
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 13 — Estado Global Reativo com Pinia Setup Stores e storeToRefs
Este exemplo apresenta a criação da Store oficial do ecossistema Vue (Pinia) utilizando a sintaxe moderna de Setup Stores (equivalente à Composition API com ref e computed) e o consumo seguro via storeToRefs para desestruturação sem perda de reatividade.
1. Definição da Store Pinia: src/stores/cartStore.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
export const useCartStore = defineStore('cart', () => {
// 1. STATE (refs reativos)
const items = ref<CartItem[]>([
{ id: 'prod-01', name: 'Teclado Mecânico Low-Profile', price: 450.0, quantity: 1 },
{ id: 'prod-02', name: 'Mouse Ergonômico Vertical', price: 280.0, quantity: 2 }
]);
const couponDiscountRate = ref<number>(0.1); // 10% de desconto inicial
// 2. GETTERS (computed properties)
const totalItemCount = computed<number>(() => {
return items.value.reduce((acc, item) => acc + item.quantity, 0);
});
const subtotalPrice = computed<number>(() => {
return items.value.reduce((acc, item) => acc + (item.price * item.quantity), 0);
});
const finalPrice = computed<number>(() => {
return subtotalPrice.value * (1 - couponDiscountRate.value);
});
// 3. ACTIONS (funções que mutam o estado diretamente)
function addItem(product: Omit<CartItem, 'quantity'>): void {
const existing = items.value.find((i) => i.id === product.id);
if (existing) {
existing.quantity++;
} else {
items.value.push({ ...product, quantity: 1 });
}
}
function removeItem(productId: string): void {
items.value = items.value.filter((i) => i.id !== productId);
}
function updateQuantity(productId: string, delta: number): void {
const target = items.value.find((i) => i.id === productId);
if (!target) return;
target.quantity += delta;
if (target.quantity <= 0) {
removeItem(productId);
}
}
function applyCoupon(code: string): boolean {
if (code.toUpperCase() === 'VUE30') {
couponDiscountRate.value = 0.3;
return true;
}
return false;
}
function clearCart(): void {
items.value = [];
}
return {
items,
couponDiscountRate,
totalItemCount,
subtotalPrice,
finalPrice,
addItem,
removeItem,
updateQuantity,
applyCoupon,
clearCart
};
});
2. Componente Consumidor: src/components/ShoppingCartView.vue
<!-- src/components/ShoppingCartView.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useCartStore } from '../stores/cartStore';
const cartStore = useCartStore();
// storeToRefs garante que getters e refs mantenham a reatividade ao serem desestruturados!
const { items, totalItemCount, subtotalPrice, finalPrice, couponDiscountRate } = storeToRefs(cartStore);
// Actions podem ser desestruturadas diretamente sem utilitários
const { updateQuantity, removeItem, applyCoupon, clearCart, addItem } = cartStore;
const couponInput = ref<string>('');
const couponFeedback = ref<string | null>(null);
function handleCouponSubmit(): void {
const success = applyCoupon(couponInput.value.trim());
if (success) {
couponFeedback.value = 'Cupom VUE30 aplicado: 30% de desconto!';
} else {
couponFeedback.value = 'Cupom inválido. Tente "VUE30".';
}
}
function addMonitor(): void {
addItem({
id: `prod-${Date.now()}`,
name: 'Monitor UltraWide 34"',
price: 2490.0
});
}
</script>
<template>
<div class="max-w-2xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
<div class="flex items-center justify-between border-b pb-4">
<div>
<h2 class="text-xl font-bold text-slate-800">🛒 Carrinho de Compras (Pinia Store)</h2>
<p class="text-xs text-slate-500">Estado compartilhado reativo e imune a perdas de referência</p>
</div>
<span class="bg-indigo-50 text-indigo-700 text-xs font-bold px-3 py-1 rounded-full">
{{ totalItemCount }} itens no carrinho
</span>
</div>
<!-- Lista de Itens do Carrinho -->
<div v-if="items.length > 0" class="divide-y border rounded-xl overflow-hidden">
<div
v-for="item in items"
:key="item.id"
class="p-4 flex items-center justify-between hover:bg-slate-50"
>
<div>
<h4 class="font-semibold text-slate-800 text-sm">{{ item.name }}</h4>
<span class="text-xs text-slate-500 font-mono">
R$ {{ item.price.toFixed(2) }} un. × {{ item.quantity }} =
<strong class="text-slate-800">R$ {{ (item.price * item.quantity).toFixed(2) }}</strong>
</span>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center border rounded-lg overflow-hidden bg-white">
<button
@click="updateQuantity(item.id, -1)"
class="px-2.5 py-1 text-xs hover:bg-slate-100 font-bold"
>
-
</button>
<span class="px-3 text-xs font-mono font-bold">{{ item.quantity }}</span>
<button
@click="updateQuantity(item.id, 1)"
class="px-2.5 py-1 text-xs hover:bg-slate-100 font-bold"
>
+
</button>
</div>
<button
@click="removeItem(item.id)"
class="text-xs text-red-500 hover:text-red-700"
title="Remover produto"
>
✕
</button>
</div>
</div>
</div>
<div v-else class="text-center py-8 bg-slate-50 border border-dashed rounded-xl text-xs text-slate-400">
O carrinho está vazio.
</div>
<!-- Seção de Cupom e Totais -->
<div class="p-4 bg-slate-50 border rounded-xl space-y-3">
<div class="flex gap-2">
<input
type="text"
v-model="couponInput"
placeholder="Código de cupom (ex: VUE30)"
class="border px-3 py-1.5 rounded-lg text-xs flex-1 uppercase font-mono"
/>
<button
@click="handleCouponSubmit"
class="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold px-4 py-1.5 rounded-lg"
>
Aplicar
</button>
</div>
<p v-if="couponFeedback" class="text-xs font-medium" :class="couponDiscountRate === 0.3 ? 'text-emerald-600' : 'text-red-600'">
{{ couponFeedback }}
</p>
<div class="pt-2 border-t space-y-1 text-xs text-slate-600">
<div class="flex justify-between">
<span>Subtotal:</span>
<span>R$ {{ subtotalPrice.toFixed(2) }}</span>
</div>
<div class="flex justify-between text-emerald-600 font-semibold">
<span>Desconto ({{ (couponDiscountRate * 100).toFixed(0) }}%):</span>
<span>- R$ {{ (subtotalPrice * couponDiscountRate).toFixed(2) }}</span>
</div>
<div class="flex justify-between text-base font-bold text-slate-900 border-t pt-2">
<span>Total Final:</span>
<span class="text-indigo-600">R$ {{ finalPrice.toFixed(2) }}</span>
</div>
</div>
</div>
<div class="flex justify-between items-center pt-2">
<button
@click="addMonitor"
class="text-xs text-indigo-600 hover:underline font-semibold"
>
+ Adicionar Monitor de Teste
</button>
<button
@click="clearCart"
class="text-xs text-red-500 hover:underline"
>
Esvaziar Carrinho
</button>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Renderização Inicial:
Total de Itens: 3
Subtotal: (450 * 1) + (280 * 2) = R$ 1.010,00
Desconto Padrão (10%): - R$ 101,00
Total Final: R$ 909,00
Interação 1 — Alterar Quantidade do Teclado (+) 1 vez:
Quantidade passa para 2. Total de Itens: 4.
Subtotal recalculado para: R$ 1.460,00. Total final: R$ 1.314,00.
Interação 2 — Aplicar Cupom "VUE30":
couponDiscountRate passa de 0.1 para 0.3 na Store.
Banner verde: "Cupom VUE30 aplicado: 30% de desconto!".
Total final recalculado para: R$ 1.022,00.
🔍 Destaques da Implementação
storeToRefsFundamental: Desestruturarconst { items } = cartStoresemstoreToRefsfariaitemsperder a reatividade de leitura. ComstoreToRefs, as variáveis permanecem sincronizadas.- Setup Store Simplicidade: Elimina mutações arcaicas do Vuex. Atribuições simples de propriedades no interior de actions são detectadas automaticamente.
- Autocompletion Completo: TypeScript infere o retorno de todos os getters e a assinatura das actions sem declarações de interfaces redundantes.