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

  1. Limpeza Mandatória em onUnmounted: Garante a neutralização de timers, WebSockets e listeners globais quando o componente é destruído.
  2. nextTick() para Sincronização do DOM: Impede cálculos errados de scrollHeight antes da reconciliação do template.
  3. Template Refs Tipadas: ref<HTMLDivElement | null>(null) oferece segurança estrita ao manipular nós HTML nativos.