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


💻 Código de Exemplo 07 — Ciclo de Vida Moderno com afterNextRender, afterRender e DestroyRef

Este exemplo apresenta a gestão moderna do ciclo de vida no Angular 18+, demonstrando como acessar o DOM com total segurança de SSR utilizando afterNextRender e registrar rotinas de descarte com DestroyRef sem necessidade do método de interface ngOnDestroy.

// src/app/components/terminal-stream-viewer.component.ts
import {
  Component,
  signal,
  ElementRef,
  viewChild,
  afterNextRender,
  afterRender,
  DestroyRef,
  inject
} from '@angular/core';

interface TelemetryPacket {
  seq: number;
  message: string;
  timestamp: string;
}

@Component({
  selector: 'app-terminal-stream-viewer',
  standalone: true,
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-4">
      <div class="flex items-center justify-between border-b pb-3">
        <div>
          <h2 class="text-lg font-bold text-slate-800">Terminal de Streaming do Cluster</h2>
          <p class="text-xs text-slate-500">Hooks de renderização pós-DOM: afterNextRender</p>
        </div>
        <div class="flex items-center gap-2">
          <span
            class="w-2.5 h-2.5 rounded-full"
            [class.bg-emerald-500]="isStreaming()"
            [class.animate-pulse]="isStreaming()"
            [class.bg-slate-300]="!isStreaming()"
          ></span>
          <button
            type="button"
            (click)="toggleStream()"
            class="text-xs font-semibold px-3 py-1.5 rounded-lg border transition-colors"
            [class]="isStreaming() ? 'bg-red-50 text-red-700 border-red-200' : 'bg-emerald-50 text-emerald-700 border-emerald-200'"
          >
            {{ isStreaming() ? 'Interromper' : 'Iniciar Stream' }}
          </button>
        </div>
      </div>

      <!-- Container monitorado com viewChild para Auto-Scroll -->
      <div
        #terminalBox
        class="h-44 bg-slate-900 text-emerald-400 p-4 rounded-xl font-mono text-xs overflow-y-auto space-y-1.5 border border-slate-800"
      >
        @for (pkt of packets(); track pkt.seq) {
          <div class="flex justify-between border-b border-slate-800/60 pb-0.5">
            <span>PKT-#{{ pkt.seq }} {{ pkt.message }}</span>
            <span class="text-slate-500 text-[10px]">{{ pkt.timestamp }}</span>
          </div>
        } @empty {
          <div class="text-slate-500">// Terminal ocioso. Inicie o streaming...</div>
        }
      </div>

      <div class="flex justify-between items-center text-xs text-slate-500 pt-1">
        <span>Total de Linhas: {{ packets().length }}</span>
        <button
          type="button"
          (click)="packets.set([])"
          class="text-xs text-slate-400 hover:text-slate-700 underline"
        >
          Limpar Terminal
        </button>
      </div>
    </div>
  `
})
export class TerminalStreamViewerComponent {
  packets = signal<TelemetryPacket[]>([]);
  isStreaming = signal<boolean>(false);

  // Modern Signal Query: viewChild substitui @ViewChild
  terminalBox = viewChild<ElementRef<HTMLDivElement>>('terminalBox');

  private timer: ReturnType<typeof setInterval> | null = null;
  private seqCounter = 0;
  private destroyRef = inject(DestroyRef);

  constructor() {
    // 1. afterNextRender: Executado UMA ÚNICA VEZ exclusivamente no navegador
    // Perfeito para inicialização de bibliotecas que exigem DOM (Chart.js, Canvas, WebGL)
    afterNextRender(() => {
      console.log('[Lifecycle] DOM montado no cliente. Inicializando visualizador com segurança SSR.');
      this.startStream();
    });

    // 2. afterRender: Executado após CADA ciclo de renderização e reconciliação do DOM
    // Garante que o scroll automático ocorra exatamente quando os novos nós já foram desenhados
    afterRender(() => {
      const box = this.terminalBox();
      if (box && this.isStreaming()) {
        box.nativeElement.scrollTop = box.nativeElement.scrollHeight;
      }
    });

    // 3. DestroyRef.onDestroy: Substitui ngOnDestroy com funções compostas
    this.destroyRef.onDestroy(() => {
      console.log('[Lifecycle] Componente destruído. Limpando intervalos e sockets.');
      this.stopStream();
    });
  }

  toggleStream(): void {
    if (this.isStreaming()) {
      this.stopStream();
    } else {
      this.startStream();
    }
  }

  private startStream(): void {
    if (this.timer) return;
    this.isStreaming.set(true);

    this.timer = setInterval(() => {
      this.seqCounter++;
      const newPacket: TelemetryPacket = {
        seq: this.seqCounter,
        message: `Heartbeat de Telemetria Nó-${(this.seqCounter % 4) + 1}`,
        timestamp: new Date().toLocaleTimeString()
      };

      this.packets.update((list) => [...list, newPacket]);
    }, 1200);
  }

  private stopStream(): void {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
    this.isStreaming.set(false);
  }
}

🖥️ Saída / Comportamento Esperado

Na Inicialização:
afterNextRender dispara exclusivamente no browser:
Console: "[Lifecycle] DOM montado no cliente. Inicializando visualizador com segurança SSR."
Timer inicia a cada 1.200ms.

A cada novo pacote emitido:
PKT-#1 Heartbeat de Telemetria Nó-2 [14:40:01]
PKT-#2 Heartbeat de Telemetria Nó-3 [14:40:02]
O hook afterRender() é executado imediatamente após o navegador inserir a nova <div> no DOM.
A caixa de scroll se mantém automaticamente no rodapé (scrollTop = scrollHeight).

Ao destruir o componente (navegar para outra tela):
DestroyRef.onDestroy executa:
Console: "[Lifecycle] Componente destruído. Limpando intervalos e sockets."
O timer setInterval é cancelado, garantindo vazamento zero de memória.

🔍 Destaques da Implementação

  1. Segurança contra Falhas de SSR: afterNextRender e afterRender nunca executam no servidor Node.js/Nitro, eliminando erros de document is not defined.
  2. DestroyRef Modular: Permite registrar rotinas de cleanup em qualquer classe ou composable injetado sem forçar a implementação de OnDestroy.
  3. Signal Queries com viewChild: Fornece referências tipadas Signal<ElementRef<T>> que reagem quando o elemento entra ou sai do DOM condicionalmente.