💻 Exemplos: Capítulo 06: Ciclo de Vida na Composition API (onMounted, onUnmounted)
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 06 — Orquestração do Ciclo de Vida e Recursos com onMounted, onUnmounted e nextTick
Este exemplo demonstra a gestão segura de efeitos externos (listeners do DOM, timers e streams simulados) com os hooks do ciclo de vida da Composition API, além de garantir renderização no DOM com nextTick().
<!-- src/components/RealtimeTelemetryWidget.vue -->
<script setup lang="ts">
import { ref, onMounted, onUpdated, onUnmounted, onErrorCaptured, nextTick } from 'vue';
interface StreamPacket {
seq: number;
timestamp: string;
latencyMs: number;
}
const packets = ref<StreamPacket[]>([]);
const isConnected = ref<boolean>(false);
const listContainerRef = ref<HTMLDivElement | null>(null);
const errorMessage = ref<string | null>(null);
let intervalTimer: ReturnType<typeof setInterval> | null = null;
let packetSequence = 0;
// 1. onMounted: Executado após a primeira montagem completa no DOM do cliente
onMounted(() => {
console.log('[Lifecycle] Componente montado com sucesso.');
startTelemetryStream();
// Registro de listener global com limpeza vinculada
window.addEventListener('resize', handleWindowResize);
});
// 2. onUpdated: Disparado após re-renderização em resposta a mudanças reativas
onUpdated(() => {
console.log(`[Lifecycle] DOM atualizado. Total de pacotes: ${packets.value.length}`);
});
// 3. onUnmounted: Limpeza OBRIGATÓRIA para evitar Memory Leaks
onUnmounted(() => {
console.log('[Lifecycle] Desmontando componente. Liberando conexões e timers...');
stopTelemetryStream();
window.removeEventListener('resize', handleWindowResize);
});
// 4. onErrorCaptured: Barreira de erro (Error Boundary) no nível de componente
onErrorCaptured((err, instance, info) => {
errorMessage.value = `Erro interceptado: ${(err as Error).message} em ${info}`;
return false; // Interrompe propagação superior
});
function handleWindowResize(): void {
console.log(`[Resize] Viewport: ${window.innerWidth}x${window.innerHeight}`);
}
function startTelemetryStream(): void {
isConnected.value = true;
intervalTimer = setInterval(async () => {
packetSequence++;
const newPacket: StreamPacket = {
seq: packetSequence,
timestamp: new Date().toLocaleTimeString(),
latencyMs: Math.floor(Math.random() * 80) + 10
};
packets.value.push(newPacket);
// Limita o histórico a 8 itens na visualização
if (packets.value.length > 8) {
packets.value.shift();
}
// nextTick garante que o scroll automático ocorra APÓS o DOM inserir o novo <li>
await nextTick();
if (listContainerRef.value) {
listContainerRef.value.scrollTop = listContainerRef.value.scrollHeight;
}
}, 1000);
}
function stopTelemetryStream(): void {
if (intervalTimer) {
clearInterval(intervalTimer);
intervalTimer = null;
}
isConnected.value = false;
}
function toggleConnection(): void {
if (isConnected.value) {
stopTelemetryStream();
} else {
startTelemetryStream();
}
}
</script>
<template>
<div class="max-w-lg 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">
<div class="flex items-center gap-2">
<span
class="w-3 h-3 rounded-full"
:class="isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-slate-300'"
></span>
<h3 class="font-bold text-slate-800">Telemetria de Streaming em Tempo Real</h3>
</div>
<button
@click="toggleConnection"
class="text-xs px-3 py-1.5 rounded-md font-semibold transition-colors"
:class="isConnected ? 'bg-red-50 text-red-700 hover:bg-red-100' : 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'"
>
{{ isConnected ? 'Interromper' : 'Conectar' }}
</button>
</div>
<div v-if="errorMessage" class="p-3 bg-red-100 text-red-800 text-xs rounded-lg">
{{ errorMessage }}
</div>
<!-- Container monitorado via Ref de Template com auto-scroll via nextTick -->
<div
ref="listContainerRef"
class="bg-slate-900 text-emerald-400 p-4 rounded-xl font-mono text-xs h-48 overflow-y-auto space-y-2 border border-slate-800"
>
<div v-if="packets.length === 0" class="text-slate-500">// Aguardando conexão de telemetria...</div>
<div
v-for="pkt in packets"
:key="pkt.seq"
class="flex justify-between items-center border-b border-slate-800/60 pb-1"
>
<span>PKT-#{{ pkt.seq.toString().padStart(4, '0') }} [{{ pkt.timestamp }}]</span>
<span :class="pkt.latencyMs > 60 ? 'text-amber-400 font-bold' : 'text-emerald-400'">
{{ pkt.latencyMs }} ms
</span>
</div>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
No momento da montagem (onMounted):
Console: "[Lifecycle] Componente montado com sucesso."
Status: Led verde pulsante ativado (bg-emerald-500 animate-pulse).
Timer inicia geração a cada 1000ms.
A cada segundo:
PKT-#0001 [10:15:30] 24 ms
PKT-#0002 [10:15:31] 72 ms (destacado em âmbar por latência alta)
nextTick() aguarda inserção no DOM e rola o listContainerRef até o final automaticamente.
Ao clicar em "Interromper":
Timer é suspenso via clearInterval.
Led torna-se cinza estático.
Ao navegar para outra rota (onUnmounted):
Console: "[Lifecycle] Desmontando componente. Liberando conexões e timers..."
Event listener de window.resize removido, garantindo zero vazamento de memória.
🔍 Destaques da Implementação
- Limpeza Mandatória em
onUnmounted: Garante a neutralização de timers, WebSockets e listeners globais quando o componente é destruído. nextTick()para Sincronização do DOM: Impede cálculos errados descrollHeightantes da reconciliação do template.- Template Refs Tipadas:
ref<HTMLDivElement | null>(null)oferece segurança estrita ao manipular nós HTML nativos.