💻 Exemplos: Capítulo 08: Criação de Composables Reutilizáveis (Custom Vue Hooks)
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
onScopeDisposeao invés deonUnmounted: Garante que composables possam ser executados tanto dentro de componentes quanto em escopos isolados do Pinia (effectScope).- Defensividade contra SSR: Checagens explícitas de
typeof window !== 'undefined'evitam erros fatais durante Server-Side Rendering (Nuxt 3). - Imutabilidade e Tipagem Forte: Uso de Generics
<T>garante integridade de contratos JSON.