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

  1. Padrão de Extensibilidade Limpa: pinia.use() permite plugar telemetria, analytics, criptografia e persistência sem alterar o código das stores.
  2. store.$patch Atômico: Aplica mútiplas propriedades de uma só vez, evitando múltiplos triggers de reatividade.
  3. TypeScript Module Augmentation: A interface DefineStoreOptionsBase garante validação estática de opções adicionais nas stores.