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


💻 Código de Exemplo 12 — Ponte Reativa com toSignal() e toObservable()

Este exemplo apresenta a interoperabilidade oficial entre o mundo assíncrono baseado em eventos do RxJS e a reatividade síncrona com rastreamento de grafos do Angular Signals através das funções toSignal() e toObservable() do pacote @angular/core/rxjs-interop.

// src/app/components/signal-rxjs-bridge.component.ts
import { Component, signal, computed, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { interval, of } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';

interface CurrencyQuote {
  pair: string;
  rate: number;
  updatedAt: string;
}

@Component({
  selector: 'app-signal-rxjs-bridge',
  standalone: true,
  imports: [CommonModule],
  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">🌉 Ponte Interoperável: Signals ⟷ RxJS</h2>
        <p class="text-xs text-slate-500">toSignal() para consumo síncrono e toObservable() para reações</p>
      </div>

      <!-- Seletor em Signal Puro -->
      <div>
        <label class="block text-xs font-semibold text-slate-700 mb-1">
          Selecionar Par de Moedas (Signal Input):
        </label>
        <div class="flex gap-2">
          <button
            type="button"
            (click)="selectedPair.set('USD/BRL')"
            [class]="selectedPair() === 'USD/BRL' ? 'bg-red-600 text-white' : 'bg-slate-100 text-slate-700'"
            class="px-3 py-1.5 rounded-lg text-xs font-bold transition-colors"
          >
            USD / BRL
          </button>
          <button
            type="button"
            (click)="selectedPair.set('EUR/BRL')"
            [class]="selectedPair() === 'EUR/BRL' ? 'bg-red-600 text-white' : 'bg-slate-100 text-slate-700'"
            class="px-3 py-1.5 rounded-lg text-xs font-bold transition-colors"
          >
            EUR / BRL
          </button>
          <button
            type="button"
            (click)="selectedPair.set('BTC/USD')"
            [class]="selectedPair() === 'BTC/USD' ? 'bg-red-600 text-white' : 'bg-slate-100 text-slate-700'"
            class="px-3 py-1.5 rounded-lg text-xs font-bold transition-colors"
          >
            BTC / USD
          </button>
        </div>
      </div>

      <!-- 1. Consumo do toSignal: Sem necessidade de AsyncPipe no template! -->
      <div class="p-5 bg-slate-50 border rounded-2xl space-y-2">
        <div class="flex justify-between items-center">
          <span class="text-xs text-slate-500 font-semibold uppercase">Cotação Atual (toSignal):</span>
          <span class="text-xs font-mono text-slate-400">Par: {{ selectedPair() }}</span>
        </div>

        <div class="flex items-baseline gap-2">
          <span class="text-3xl font-black text-slate-900 font-mono">
            {{ currentQuote().rate.toFixed(4) }}
          </span>
          <span class="text-xs text-slate-500 font-medium">BRL</span>
        </div>

        <div class="text-[11px] text-slate-400 font-mono pt-1 border-t flex justify-between">
          <span>Último Tick RxJS: {{ currentQuote().updatedAt }}</span>
          <span>Pulso Periódico: 2.5s</span>
        </div>
      </div>

      <!-- 2. Computed Signal derivando cálculo a partir do Signal convertido -->
      <div class="p-4 bg-indigo-50/60 border border-indigo-100 rounded-xl flex justify-between items-center text-xs">
        <div>
          <span class="font-bold text-indigo-950">Volume Estimado (10.000 unidades):</span>
          <p class="text-indigo-700 font-mono text-sm font-extrabold mt-0.5">
            R$ {{ totalConvertedVolume().toLocaleString('pt-BR', { minimumFractionDigits: 2 }) }}
          </p>
        </div>
        <span class="px-2.5 py-1 bg-white border border-indigo-200 text-indigo-800 rounded-lg font-bold">
          Calculado via computed()
        </span>
      </div>
    </div>
  `
})
export class SignalRxjsBridgeComponent {
  // A. Signal Inicial controlado pela UI
  selectedPair = signal<'USD/BRL' | 'EUR/BRL' | 'BTC/USD'>('USD/BRL');

  // B. toObservable(): Converte o Signal em um Observable que emite a cada mudança de par
  private selectedPair$ = toObservable(this.selectedPair);

  // C. Pipeline RxJS que reage às mudanças do Signal e gera stream periódico
  private quoteStream$ = this.selectedPair$.pipe(
    switchMap((pair) =>
      interval(2500).pipe(
        map(() => this.calculateMockQuote(pair)),
        catchError(() => of({ pair, rate: 0, updatedAt: 'Erro' }))
      )
    )
  );

  // D. toSignal(): Converte o stream do RxJS de volta para um Signal síncrono limpo!
  // Dispensa o uso de AsyncPipe no HTML e automatiza a desinscrição no desmonte
  currentQuote = toSignal(this.quoteStream$, {
    initialValue: {
      pair: 'USD/BRL',
      rate: 5.624,
      updatedAt: 'Inicializado'
    }
  });

  // E. Computed Signal derivado sem qualquer complexidade de streams
  totalConvertedVolume = computed<number>(() => {
    return this.currentQuote().rate * 10000;
  });

  private calculateMockQuote(pair: string): CurrencyQuote {
    const baseRates: Record<string, number> = {
      'USD/BRL': 5.62,
      'EUR/BRL': 6.18,
      'BTC/USD': 64200.0
    };
    const base = baseRates[pair] ?? 1.0;
    const variation = (Math.random() * 0.04) - 0.02; // +/- 2%
    return {
      pair,
      rate: Number((base * (1 + variation)).toFixed(4)),
      updatedAt: new Date().toLocaleTimeString()
    };
  }
}

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Par Selecionado: USD/BRL
Cotação Inicial (toSignal): 5.6240 BRL (Atualizado em: Inicializado)
Volume Estimado (computed): R$ 56.240,00

Após 2.5 segundos:
O interval do RxJS emite uma nova cotação (ex: 5.6310 BRL).
toSignal() sincroniza o valor sem necessidade de AsyncPipe no template.
O computed() totalConvertedVolume reage instantaneamente recalculando para R$ 56.310,00.

Ao clicar em "EUR / BRL":
selectedPair signal é atualizado para 'EUR/BRL'.
toObservable() propaga o novo valor no stream RxJS.
switchMap cancela o timer anterior e inicia a geração para EUR/BRL.
O valor salta para ~6.1800 BRL e o volume para R$ 61.800,00.

🔍 Destaques da Implementação

  1. toSignal(): Permite que APIs baseadas em RxJS (como HttpClient, ActivatedRoute.params) sejam lidas em templates sem o operador | async.
  2. toObservable(): Permite aplicar operadores de tempo (debounceTime, auditTime) sobre valores gerados por Signals.
  3. Limpeza Automática: toSignal cancela a subscrição do Observable subjacente assim que o componente é destruído.