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


💻 Código de Exemplo 06 — Reatividade com signal(), computed(), effect() e untracked()

Este exemplo apresenta a utilização dos três pilares da reatividade de Signals no Angular 18/19: signal (estado primitivo ou estruturado mutável), computed (derivação memoizada livre de efeitos colaterais) e effect (efeitos colaterais assíncronos com limpeza via onCleanup).

// src/app/components/signal-analytics-engine.component.ts
import { Component, signal, computed, effect, untracked } from '@angular/core';

interface TelemetrySample {
  cpuUsage: number;
  memoryUsage: number;
  temperature: number;
}

@Component({
  selector: 'app-signal-analytics-engine',
  standalone: true,
  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="border-b pb-4">
        <h2 class="text-xl font-bold text-slate-800">⚡ Motor Analítico com Angular Signals</h2>
        <p class="text-xs text-slate-500">Reatividade fina sem Zone.js (Zoneless Foundation)</p>
      </div>

      <!-- Métricas Primárias em Sinais -->
      <div class="grid grid-cols-3 gap-3">
        <div class="p-3 bg-slate-50 border rounded-xl text-center">
          <span class="text-[10px] uppercase font-bold text-slate-400">CPU Usage</span>
          <div class="text-xl font-black text-slate-900 mt-1">{{ telemetry().cpuUsage }}%</div>
        </div>

        <div class="p-3 bg-slate-50 border rounded-xl text-center">
          <span class="text-[10px] uppercase font-bold text-slate-400">Memória RAM</span>
          <div class="text-xl font-black text-slate-900 mt-1">{{ telemetry().memoryUsage }}%</div>
        </div>

        <div class="p-3 bg-slate-50 border rounded-xl text-center">
          <span class="text-[10px] uppercase font-bold text-slate-400">Temperatura</span>
          <div
            class="text-xl font-black mt-1"
            [class.text-red-600]="telemetry().temperature > 75"
            [class.text-emerald-600]="telemetry().temperature <= 75"
          >
            {{ telemetry().temperature }}°C
          </div>
        </div>
      </div>

      <!-- Derivações Computadas com Computed Signals -->
      <div class="p-4 bg-slate-50 border rounded-xl space-y-2">
        <div class="flex justify-between items-center text-xs">
          <span class="text-slate-600 font-medium">Índice Geral de Estresse (0-100):</span>
          <strong class="text-slate-900 font-mono text-sm">{{ stressIndex() }} pts</strong>
        </div>

        <div class="flex justify-between items-center text-xs">
          <span class="text-slate-600 font-medium">Classificação Operacional:</span>
          <span
            class="px-2.5 py-0.5 rounded-full font-bold text-[10px]"
            [class.bg-emerald-100]="healthGrade() === 'ÓTIMO'"
            [class.text-emerald-800]="healthGrade() === 'ÓTIMO'"
            [class.bg-amber-100]="healthGrade() === 'ALERTA'"
            [class.text-amber-800]="healthGrade() === 'ALERTA'"
            [class.bg-red-100]="healthGrade() === 'CRÍTICO'"
            [class.text-red-800]="healthGrade() === 'CRÍTICO'"
          >
            {{ healthGrade() }}
          </span>
        </div>
      </div>

      <!-- Controles de Ação com .set() e .update() -->
      <div class="flex gap-2">
        <button
          type="button"
          (click)="injectLoadSpike()"
          class="flex-1 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-xs font-bold transition-colors"
        >
          Simular Pico de Carga (.update)
        </button>
        <button
          type="button"
          (click)="resetCooling()"
          class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl text-xs font-semibold"
        >
          Resfriar (.set)
        </button>
      </div>

      <!-- Log de Efeitos Gerados pelo effect() -->
      <div class="space-y-1">
        <span class="text-[10px] font-bold uppercase text-slate-400 tracking-wider">
          Auditoria de Efeitos Reativos (effect):
        </span>
        <div class="p-3 bg-slate-900 text-emerald-400 font-mono text-xs rounded-xl h-28 overflow-y-auto space-y-1">
          @for (log of auditLogs(); track $index) {
            <div>{{ log }}</div>
          }
        </div>
      </div>
    </div>
  `
})
export class SignalAnalyticsEngineComponent {
  // 1. Sinal Gravável Principal (Writable Signal)
  telemetry = signal<TelemetrySample>({
    cpuUsage: 35,
    memoryUsage: 48,
    temperature: 52
  });

  auditLogs = signal<string[]>([]);
  private sessionTag = 'PROD-ZONE-1';

  // 2. Computed Signal (Cálculo Puro Memoizado)
  stressIndex = computed<number>(() => {
    const { cpuUsage, memoryUsage, temperature } = this.telemetry();
    // Fórmula ponderada de estresse
    const raw = (cpuUsage * 0.4) + (memoryUsage * 0.3) + ((temperature / 100) * 30);
    return Math.round(raw);
  });

  healthGrade = computed<'ÓTIMO' | 'ALERTA' | 'CRÍTICO'>(() => {
    const index = this.stressIndex();
    if (index >= 75) return 'CRÍTICO';
    if (index >= 50) return 'ALERTA';
    return 'ÓTIMO';
  });

  constructor() {
    // 3. effect(): Executado automaticamente sempre que dependências mudarem
    effect((onCleanup) => {
      const currentGrade = this.healthGrade();
      const currentStress = this.stressIndex();

      // untracked: Lê um dado sem cadastrá-lo como gatilho do efeito
      const tag = untracked(() => this.sessionTag);

      const logMsg = `[${new Date().toLocaleTimeString()}] [${tag}] Status: ${currentGrade} (Índice: ${currentStress})`;

      this.auditLogs.update((curr) => [logMsg, ...curr.slice(0, 7)]);

      onCleanup(() => {
        // Executado imediatamente antes da próxima execução do efeito ou destruição
      });
    });
  }

  // 4. Mutações com .update()
  injectLoadSpike(): void {
    this.telemetry.update((prev) => ({
      cpuUsage: Math.min(100, prev.cpuUsage + 25),
      memoryUsage: Math.min(100, prev.memoryUsage + 15),
      temperature: Math.min(95, prev.temperature + 12)
    }));
  }

  // 5. Mutações com .set()
  resetCooling(): void {
    this.telemetry.set({
      cpuUsage: 20,
      memoryUsage: 35,
      temperature: 42
    });
  }
}

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
CPU: 35% | RAM: 48% | Temp: 52°C
Índice de Estresse: 44 pts | Classificação: [ÓTIMO] verde
Log Inicial no Terminal Escuro:
"[14:35:10] [PROD-ZONE-1] Status: ÓTIMO (Índice: 44)"

Interação 1 — Clicar em "Simular Pico de Carga" (1ª vez):
CPU salta para 60%, RAM para 63%, Temp para 64°C.
Índice de Estresse computado: 62 pts.
Classificação muda para [ALERTA] âmbar.
Novo log inserido no topo via effect().

Interação 2 — Clicar em "Simular Pico de Carga" (2ª vez):
CPU salta para 85%, RAM para 78%, Temp para 76°C (destacado em vermelho).
Índice de Estresse: 80 pts. Classificação: [CRÍTICO] vermelho.
Novo log de auditoria gerado instantaneamente.

Interação 3 — Clicar em "Resfriar (.set)":
Valores retornam a 20%, 35%, 42°C.
Índice recua para 27 pts e classificação volta para ÓTIMO.

🔍 Destaques da Implementação

  1. Grafo de Dependências sem Zone.js: O Angular sabe cirurgicamente quais nós do DOM dependem de healthGrade(), sem disparar detecção de mudanças sujas em toda a árvore.
  2. Memoização Automática: Se telemetry for atualizado mas o resultado do cálculo em stressIndex for idêntico ao anterior, os consumidores do healthGrade não são reavaliados.
  3. untracked() Estratégico: Impede que alterações em variáveis secundárias disparem o callback do effect.