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


💻 Código de Exemplo 10 — Streams Reativos com RxJS, BehaviorSubject e takeUntilDestroyed()

Este exemplo apresenta os fundamentos da programação reativa assíncrona com RxJS no Angular 18+, utilizando BehaviorSubject, renderização declarativa com o AsyncPipe e cancelamento automático de subscrições em tempo de destruição com takeUntilDestroyed().

// src/app/components/network-status-monitor.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule, AsyncPipe } from '@angular/common';
import { BehaviorSubject, interval, Observable } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

interface NetworkMetric {
  latencyMs: number;
  packetLossRate: number;
  status: 'OPTIMAL' | 'DEGRADED' | 'DISCONNECTED';
}

@Component({
  selector: 'app-network-status-monitor',
  standalone: true,
  imports: [CommonModule, AsyncPipe],
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-6">
      <div class="flex items-center justify-between border-b pb-4">
        <div>
          <h2 class="text-xl font-bold text-slate-800">📡 Monitor de Conectividade RxJS</h2>
          <p class="text-xs text-slate-500">Streams com BehaviorSubject e AsyncPipe</p>
        </div>
        <div class="flex gap-2">
          <button
            type="button"
            (click)="simulateDegradation()"
            class="text-xs px-3 py-1.5 bg-amber-50 text-amber-800 border border-amber-200 rounded-lg font-semibold hover:bg-amber-100"
          >
            Simular Degradação
          </button>
          <button
            type="button"
            (click)="resetOptimal()"
            class="text-xs px-3 py-1.5 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-lg font-semibold hover:bg-emerald-100"
          >
            Restaurar Ótimo
          </button>
        </div>
      </div>

      <!-- CONSUMO DECLARATIVO DO OBSERVABLE COM ASYNCPIPE -->
      @if (metric$ | async; as metric) {
        <div class="grid grid-cols-3 gap-3">
          <div class="p-4 bg-slate-50 border rounded-xl text-center">
            <span class="text-[10px] uppercase font-bold text-slate-400">Latência TCP</span>
            <div class="text-xl font-black text-slate-900 mt-1">{{ metric.latencyMs }} ms</div>
          </div>

          <div class="p-4 bg-slate-50 border rounded-xl text-center">
            <span class="text-[10px] uppercase font-bold text-slate-400">Perda de Pacotes</span>
            <div class="text-xl font-black text-slate-900 mt-1">
              {{ (metric.packetLossRate * 100).toFixed(1) }}%
            </div>
          </div>

          <div class="p-4 bg-slate-50 border rounded-xl text-center">
            <span class="text-[10px] uppercase font-bold text-slate-400">Qualidade da Rede</span>
            <div
              class="text-base font-black mt-1 uppercase"
              [ngClass]="{
                'text-emerald-600': metric.status === 'OPTIMAL',
                'text-amber-600': metric.status === 'DEGRADED',
                'text-red-600': metric.status === 'DISCONNECTED'
              }"
            >
              {{ metric.status }}
            </div>
          </div>
        </div>
      } @else {
        <div class="p-6 text-center text-xs text-slate-400 bg-slate-50 border rounded-xl">
          Aguardando emissão do primeiro frame de telemetria RxJS...
        </div>
      }

      <!-- Painel de Logs Escutados Manualmente via takeUntilDestroyed -->
      <div class="space-y-1">
        <span class="text-[10px] font-bold uppercase text-slate-400">
          Logs de Auditoria RxJS (Inscrição protegida por takeUntilDestroyed):
        </span>
        <div class="p-3 bg-slate-900 text-emerald-400 font-mono text-xs rounded-xl h-24 overflow-y-auto space-y-1">
          @for (log of auditEvents; track $index) {
            <div>{{ log }}</div>
          }
        </div>
      </div>
    </div>
  `
})
export class NetworkStatusMonitorComponent {
  // 1. BehaviorSubject: Mantém o último valor emitido e o entrega imediatamente a novos inscritos
  private metricSubject = new BehaviorSubject<NetworkMetric>({
    latencyMs: 18,
    packetLossRate: 0.001,
    status: 'OPTIMAL'
  });

  // 2. Observable Público exposto para consumo seguro (somente leitura)
  readonly metric$: Observable<NetworkMetric> = this.metricSubject.asObservable();

  auditEvents: string[] = [];

  constructor() {
    // 3. Inscrição Imperativa Segura com takeUntilDestroyed()
    // Cancela a assinatura automaticamente quando o componente for destruído pelo Angular
    this.metric$
      .pipe(takeUntilDestroyed())
      .subscribe((data) => {
        const msg = `[${new Date().toLocaleTimeString()}] Status alterado para: ${data.status} (Latência: ${data.latencyMs}ms)`;
        this.auditEvents.unshift(msg);
      });
  }

  simulateDegradation(): void {
    this.metricSubject.next({
      latencyMs: 145,
      packetLossRate: 0.048,
      status: 'DEGRADED'
    });
  }

  resetOptimal(): void {
    this.metricSubject.next({
      latencyMs: 16,
      packetLossRate: 0.0,
      status: 'OPTIMAL'
    });
  }
}

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
AsyncPipe se inscreve no metric$:
Latência: 18 ms | Perda: 0.1% | Qualidade: [OPTIMAL] verde
Audit Log: "[14:52:00] Status alterado para: OPTIMAL (Latência: 18ms)"

Ao clicar em "Simular Degradação":
metricSubject.next(...) emite um novo objeto no stream.
O AsyncPipe detecta a emissão e atualiza o DOM instantaneamente:
Latência: 145 ms | Perda: 4.8% | Qualidade: [DEGRADED] âmbar
Novo evento adicionado à lista de logs auditEvents.

Ao navegar para outra rota (destruição do componente):
takeUntilDestroyed() encerra a subscrição automaticamente sem necessidade de unsubscribe() manual.
O AsyncPipe se desinscreve nativamente, garantindo descarte completo da memória.

🔍 Destaques da Implementação

  1. takeUntilDestroyed() (Angular 16+): Utiliza o contexto de injeção atual para se vincular ao DestroyRef do componente, tornando o padrão Subject + takeUntil obsoleto.
  2. AsyncPipe Gerenciador: Automatiza a subscrição, a desinscrição e a marcação de detecção de mudanças (markForCheck) sem código imperativo no .ts.
  3. Encapsulamento com asObservable(): Impede que consumidores externos chamem .next() indevidamente na fonte de dados.