💻 Exemplos: Capítulo 12: Interoperabilidade entre Signals e RxJS (toSignal, toObservable)
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
toSignal(): Permite que APIs baseadas em RxJS (comoHttpClient,ActivatedRoute.params) sejam lidas em templates sem o operador| async.toObservable(): Permite aplicar operadores de tempo (debounceTime,auditTime) sobre valores gerados por Signals.- Limpeza Automática:
toSignalcancela a subscrição do Observable subjacente assim que o componente é destruído.