💻 Exemplos: Capítulo 14: Plugins do Pinia, Persistência de Estado e Setup Stores
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 14 — Plugin Customizado de Persistência com pinia.use e $subscribe
Este exemplo apresenta a criação de um plugin corporativo para o Pinia que persiste automaticamente qualquer store anotada com a flag persist: true no localStorage, além de gerenciar eventos de sincronização entre abas do navegador.
1. Definição do Plugin de Persistência: src/plugins/piniaPersistencePlugin.ts
import type { PiniaPluginContext } from 'pinia';
// 1. Extensão de tipos do Pinia para aceitar a opção 'persist' nas stores
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
persist?: boolean | { key?: string; storage?: 'local' | 'session' };
}
}
export function piniaPersistencePlugin({ store, options }: PiniaPluginContext): void {
if (!options.persist) return;
const storageKey =
typeof options.persist === 'object' && options.persist.key
? options.persist.key
: `pinia_store_${store.$id}`;
const storageEngine =
typeof options.persist === 'object' && options.persist.storage === 'session'
? window.sessionStorage
: window.localStorage;
// A. Hidratação Inicial a partir do disco
try {
const rawData = storageEngine.getItem(storageKey);
if (rawData) {
store.$patch(JSON.parse(rawData));
console.log(`[PiniaPlugin] Store "${store.$id}" hidratada a partir de ${storageKey}`);
}
} catch (err) {
console.error(`[PiniaPlugin] Falha ao hidratar store "${store.$id}":`, err);
}
// B. Subscrição Reativa a qualquer alteração de estado
store.$subscribe(
(_mutation, state) => {
try {
storageEngine.setItem(storageKey, JSON.stringify(state));
} catch (err) {
console.error(`[PiniaPlugin] Erro ao sincronizar store "${store.$id}":`, err);
}
},
{ detached: true }
);
// C. Sincronização entre múltiplas abas do navegador
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key === storageKey && event.newValue) {
console.log(`[PiniaPlugin] Sincronização multi-aba detectada para "${store.$id}"`);
store.$patch(JSON.parse(event.newValue));
}
});
}
2. Store Anotada: src/stores/authSessionStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthSessionStore = defineStore(
'authSession',
() => {
const token = ref<string | null>(null);
const userEmail = ref<string>('');
const lastLoginTime = ref<string>('');
function login(email: string): void {
token.value = `jwt_mock_${Date.now()}`;
userEmail.value = email;
lastLoginTime.value = new Date().toISOString();
}
function logout(): void {
token.value = null;
userEmail.value = '';
lastLoginTime.value = '';
}
return { token, userEmail, lastLoginTime, login, logout };
},
{
// Opção lida pelo nosso plugin customizado
persist: { key: 'corp_auth_session', storage: 'local' }
}
);
3. Componente de Teste: SessionSyncTester.vue
<!-- src/components/SessionSyncTester.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useAuthSessionStore } from '../stores/authSessionStore';
const authStore = useAuthSessionStore();
const { token, userEmail, lastLoginTime } = storeToRefs(authStore);
const inputEmail = ref<string>('diretoria@empresa.com.br');
function handleLogin(): void {
authStore.login(inputEmail.value);
}
function handleLogout(): void {
authStore.logout();
}
</script>
<template>
<div class="max-w-md mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-4">
<div class="flex items-center justify-between border-b pb-3">
<h3 class="font-bold text-slate-800">Sessão Persistente (Pinia Plugin)</h3>
<span
class="text-xs px-2 py-0.5 rounded-full font-bold"
:class="token ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600'"
>
{{ token ? 'Autenticado' : 'Offline' }}
</span>
</div>
<div v-if="token" class="space-y-2 bg-slate-50 p-4 rounded-xl border text-xs">
<p><strong>E-mail:</strong> {{ userEmail }}</p>
<p><strong>Login em:</strong> {{ lastLoginTime }}</p>
<p class="font-mono text-slate-500 truncate"><strong>Token:</strong> {{ token }}</p>
<button
@click="handleLogout"
class="mt-2 w-full py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-semibold"
>
Encerrar Sessão (Limpar Persistência)
</button>
</div>
<div v-else class="space-y-3">
<div>
<label class="block text-xs font-semibold text-slate-600 mb-1">E-mail para Login:</label>
<input
type="email"
v-model="inputEmail"
class="w-full border px-3 py-2 rounded-lg text-sm"
/>
</div>
<button
@click="handleLogin"
class="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-semibold text-sm"
>
Simular Login e Persistir
</button>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Cenário 1 — Primeira Carga:
O plugin verifica localStorage.getItem('corp_auth_session'). Como não existe, a store permanece com valores padrão (token: null).
Componente exibe formulário de login.
Cenário 2 — Ação de Login:
Clicar em "Simular Login e Persistir".
authStore.login() atualiza token, userEmail e lastLoginTime.
store.$subscribe() dispara automaticamente e salva no localStorage em 'corp_auth_session'.
Badge muda para "Autenticado" (verde).
Cenário 3 — Recarregar a página (F5) ou Abrir em Outra Aba:
Ao reiniciar a aplicação, piniaPersistencePlugin reidrata o estado instantaneamente via store.$patch().
Se o usuário deslogar em uma aba, o evento 'storage' é acionado na outra aba, refletindo o logout em tempo real sem refresh.
🔍 Destaques da Implementação
- Padrão de Extensibilidade Limpa:
pinia.use()permite plugar telemetria, analytics, criptografia e persistência sem alterar o código das stores. store.$patchAtômico: Aplica mútiplas propriedades de uma só vez, evitando múltiplos triggers de reatividade.- TypeScript Module Augmentation: A interface
DefineStoreOptionsBasegarante validação estática de opções adicionais nas stores.