Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 08 — Construção de Composables Robustos com onScopeDispose

Este exemplo apresenta a criação de dois composables corporativos reutilizáveis: useLocalStorage (sincronização reativa e tipada com localStorage) e useOnlineStatus (detecção de conectividade com liberação automática de memória via onScopeDispose).

1. Composable: src/composables/useLocalStorage.ts

import { ref, watch, type Ref } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
  // Leitura inicial segura contra falhas de SSR ou parse
  const readValue = (): T => {
    if (typeof window === 'undefined') return defaultValue;
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? (JSON.parse(stored) as T) : defaultValue;
    } catch (err) {
      console.warn(`[useLocalStorage] Erro ao ler a chave "${key}":`, err);
      return defaultValue;
    }
  };

  const data = ref<T>(readValue()) as Ref<T>;

  // Sincronização automática com localStorage quando o dado reativo mudar
  watch(
    data,
    (newVal) => {
      if (typeof window === 'undefined') return;
      try {
        window.localStorage.setItem(key, JSON.stringify(newVal));
      } catch (err) {
        console.error(`[useLocalStorage] Erro ao persistir chave "${key}":`, err);
      }
    },
    { deep: true }
  );

  return data;
}

2. Composable: src/composables/useOnlineStatus.ts

import { ref, onScopeDispose, type Ref } from 'vue';

export interface NetworkStatus {
  isOnline: Ref<boolean>;
  downlinkSpeedMb: Ref<number | null>;
}

export function useOnlineStatus(): NetworkStatus {
  const isOnline = ref<boolean>(typeof navigator !== 'undefined' ? navigator.onLine : true);
  const downlinkSpeedMb = ref<number | null>(null);

  const updateNetwork = () => {
    isOnline.value = navigator.onLine;
    const connection = (navigator as any).connection;
    if (connection) {
      downlinkSpeedMb.value = connection.downlink ?? null;
    }
  };

  if (typeof window !== 'undefined') {
    window.addEventListener('online', updateNetwork);
    window.addEventListener('offline', updateNetwork);
    updateNetwork();

    // onScopeDispose: Limpa listeners se o composable for chamado dentro de setup() ou effectScope
    onScopeDispose(() => {
      console.log('[useOnlineStatus] Liberando listeners globais de rede.');
      window.removeEventListener('online', updateNetwork);
      window.removeEventListener('offline', updateNetwork);
    });
  }

  return { isOnline, downlinkSpeedMb };
}

3. Componente Consumidor: UserSettingsCard.vue

<!-- src/components/UserSettingsCard.vue -->
<script setup lang="ts">
import { useLocalStorage } from '../composables/useLocalStorage';
import { useOnlineStatus } from '../composables/useOnlineStatus';

interface AppPreferences {
  theme: 'dark' | 'light' | 'system';
  fontSizePx: number;
  autoSave: boolean;
}

// Consumo do composable de persistência
const preferences = useLocalStorage<AppPreferences>('corp_portal_prefs', {
  theme: 'system',
  fontSizePx: 14,
  autoSave: true
});

// Consumo do composable de rede
const { isOnline, downlinkSpeedMb } = useOnlineStatus();

function resetDefaults(): void {
  preferences.value = {
    theme: 'system',
    fontSizePx: 14,
    autoSave: true
  };
}
</script>

<template>
  <div class="max-w-md mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-5">
    <div class="flex items-center justify-between border-b pb-3">
      <h3 class="font-bold text-slate-800">Preferências do Sistema</h3>
      <div class="flex items-center gap-1.5 text-xs font-semibold">
        <span
          class="w-2.5 h-2.5 rounded-full"
          :class="isOnline ? 'bg-emerald-500' : 'bg-red-500'"
        ></span>
        <span :class="isOnline ? 'text-emerald-700' : 'text-red-700'">
          {{ isOnline ? 'Online' : 'Offline' }}
          {{ downlinkSpeedMb ? `(${downlinkSpeedMb} Mbps)` : '' }}
        </span>
      </div>
    </div>

    <div class="space-y-4">
      <div>
        <label class="block text-xs font-semibold text-slate-600 mb-1">Tema da Interface:</label>
        <select
          v-model="preferences.theme"
          class="w-full border px-3 py-2 rounded-lg text-sm bg-white"
        >
          <option value="light">Claro</option>
          <option value="dark">Escuro</option>
          <option value="system">Seguir Sistema Operacional</option>
        </select>
      </div>

      <div>
        <label class="block text-xs font-semibold text-slate-600 mb-1">
          Tamanho da Fonte: {{ preferences.fontSizePx }}px
        </label>
        <input
          type="range"
          min="12"
          max="24"
          v-model.number="preferences.fontSizePx"
          class="w-full h-2 bg-slate-200 rounded-lg cursor-pointer"
        />
      </div>

      <label class="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
        <input type="checkbox" v-model="preferences.autoSave" class="rounded text-indigo-600" />
        Salvar alterações automaticamente no disco
      </label>
    </div>

    <div class="p-3 bg-slate-50 border rounded-xl flex items-center justify-between">
      <span class="text-xs text-slate-500 font-mono">Chave: corp_portal_prefs</span>
      <button
        @click="resetDefaults"
        class="text-xs text-indigo-600 hover:text-indigo-800 font-medium"
      >
        Restaurar Padrão
      </button>
    </div>
  </div>
</template>

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
useLocalStorage lê 'corp_portal_prefs' do localStorage do browser.
Se não existir, grava o padrão: { theme: 'system', fontSizePx: 14, autoSave: true }.
useOnlineStatus verifica o status da placa de rede: LED verde aceso 'Online (10 Mbps)'.

Ao interagir com o select de tema ou com o slider de fonte:
O watcher profundo em useLocalStorage grava a nova string JSON no localStorage.
Ao recarregar a página (F5), os valores são restaurados instantaneamente sem piscar a tela.

Ao desconectar o Wi-Fi:
O evento 'offline' do window é interceptado: LED torna-se vermelho 'Offline'.
Ao desmontar a view:
Console exibe: "[useOnlineStatus] Liberando listeners globais de rede."

🔍 Destaques da Implementação

  1. onScopeDispose ao invés de onUnmounted: Garante que composables possam ser executados tanto dentro de componentes quanto em escopos isolados do Pinia (effectScope).
  2. Defensividade contra SSR: Checagens explícitas de typeof window !== 'undefined' evitam erros fatais durante Server-Side Rendering (Nuxt 3).
  3. Imutabilidade e Tipagem Forte: Uso de Generics <T> garante integridade de contratos JSON.