💻 Exemplos: Capítulo 07: Ciclo de Vida de Componentes e Hooks Modernos (afterRender)
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
- Segurança contra Falhas de SSR:
afterNextRendereafterRendernunca executam no servidor Node.js/Nitro, eliminando erros dedocument is not defined. DestroyRefModular: Permite registrar rotinas de cleanup em qualquer classe ou composable injetado sem forçar a implementação deOnDestroy.- Signal Queries com
viewChild: Fornece referências tipadasSignal<ElementRef<T>>que reagem quando o elemento entra ou sai do DOM condicionalmente.