Capítulo 12: Interoperabilidade entre Signals e RxJS (toSignal, toObservable)
Capítulo 12: Interoperabilidade entre Signals e RxJS (toSignal, toObservable)
Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR
🗺️ Mapa Conceitual da Ponte de Interoperabilidade Reativa
flowchart LR
subgraph RXJS_REALM["Mundo RxJS (Eventos Assíncronos no Tempo)"]
HTTP["HttpClient (Cold Stream)"]
WS["WebSocket (Hot Stream)"]
RouteParams["ActivatedRoute.paramMap"]
end
subgraph INTEROP["Ponte de Interoperabilidade (@angular/core/rxjs-interop)"]
TS["toSignal(stream$, { initialValue })"]
TO["toObservable(meuSignal)"]
end
subgraph SIGNALS_REALM["Mundo Signals (Estado Síncrono de UI)"]
Comp["Componente Standalone"]
Template["Template HTML {{ meuSignal() }}"]
Computed["computed(() => ...)"]
end
RXJS_REALM -->|"toSignal()"| INTEROP
INTEROP --> SIGNALS_REALM
SIGNALS_REALM -->|"toObservable()"| INTEROP
INTEROP --> RXJS_REALM
style RXJS_REALM fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style INTEROP fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style SIGNALS_REALM fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. O Casamento Perfeito: RxJS + Signals
Uma dúvida muito frequente em equipes de desenvolvimento é: “Os Signals vieram para substituir o RxJS?”. A resposta oficial do time de engenharia do Angular é NÃO.
Ambas as primitivas cumprem propósitos complementares e coexistem em harmonia no que chamamos de Arquitetura Reativa Híbrida:
- RxJS no Backend / Serviços: Excelente para orquestração assíncrona, debounce de digitação, cancelamento de requisições, tratamento de erro, reconexão de sockets e controle de concorrência.
- Signals no Componente / Template: Perfeito para armazenar estado síncrono, expor propriedades legíveis pela view, calcular valores derivados com
computed()e acionar renderização cirúrgica sem Zone.js.
Para conectar esses dois mundos com total segurança e zero vazamento de memória, o Angular fornece o pacote @angular/core/rxjs-interop.
⚙️ 2. Convertendo Streams em Estado com toSignal()
A função toSignal() assina um Observable e transforma suas emissões em um Signal somente-leitura (Signal<T>).
2.1 Gerenciamento Automático de Memória
Quando chamado dentro de um contexto de injeção (como no construtor ou na inicialização de campos da classe), o toSignal() cancela automaticamente a subscrição quando o componente for destruído! Você nunca mais precisará escrever unsubscribe() manual para consumir dados assíncronos no template.
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
interface PerfilUsuario {
id: number;
nome: string;
email: string;
saldo: number;
}
@Component({
selector: 'app-perfil-cliente',
standalone: true,
template: `
<div class="card">
@if (usuario(); as u) {
<h2>{{ u.nome }}</h2>
<p>Email: {{ u.email }}</p>
<p>Saldo em Conta: {{ u.saldo | currency:'BRL' }}</p>
} @else {
<p>Carregando perfil do usuário...</p>
}
</div>
`
})
export class PerfilClienteComponent {
private http = inject(HttpClient);
// Observable transformado diretamente em Signal seguro e tipado!
usuario = toSignal(
this.http.get<PerfilUsuario>('https://api.banco.com/perfil').pipe(
catchError(() => of(null))
),
{ initialValue: null } // Valor inicial síncrono obrigatório para evitar tipo 'undefined'
);
}
2.2 Opções Avançadas do toSignal()
initialValue: Define o valor inicial do signal até que a primeira emissão do Observable ocorra.requireSync: true: Exige que o Observable emita sincronicamente na subscrição (ex: umBehaviorSubject). Se o Observable for assíncrono, lança uma exceção.rejectErrors: true: Faz com que erros na stream lancem exceções quando o signal for lido, em vez de ficarem silenciosos.
🔄 3. Convertendo Signals em Streams com toObservable()
Quando você tem um Signal (por exemplo, um termo digitado ou um filtro de categoria selecionado) e precisa passar esse valor por uma pipeline do RxJS contendo operadores como debounceTime, switchMap ou distinctUntilChanged, utiliza-se toObservable():
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-busca-reativa',
standalone: true,
template: `
<input
type="text"
[value]="termo()"
(input)="aoDigitar($event)"
placeholder="Digite para pesquisar..."
/>
<ul>
@for (item of resultados(); track item.id) {
<li>{{ item.titulo }}</li>
} @empty {
<li>Nenhum resultado disponível.</li>
}
</ul>
`
})
export class BuscaReativaComponent {
private http = inject(HttpClient);
// 1. Estado local em Signal
termo = signal('');
// 2. Transforma o Signal em Observable para aplicar operadores RxJS de tempo
private termo$ = toObservable(this.termo);
// 3. Orquestra a busca assíncrona com switchMap e converte o resultado de volta para Signal!
resultados = toSignal(
this.termo$.pipe(
debounceTime(350),
distinctUntilChanged(),
switchMap(q => {
if (!q || q.length < 2) return of([]);
return this.http.get<any[]>(`/api/artigos?busca=${q}`).pipe(
catchError(() => of([]))
);
})
),
{ initialValue: [] }
);
aoDigitar(event: Event): void {
const input = event.target as HTMLInputElement;
this.termo.set(input.value);
}
}
⚖️ 4. Matriz Comparativa: toSignal vs toObservable
| Critério | toSignal() |
toObservable() |
|---|---|---|
| Sentido da Conversão | Observable<T> ➔ Signal<T> |
Signal<T> ➔ Observable<T> |
| Momento de Atualização | Imediato à emissão do Observable | Agendado em microtask assíncrona |
| Cancelamento de Recursos | Automático via DestroyRef |
Automático via ciclo de vida do RxJS |
| Casos Típicos | Consumir chamadas HTTP e Stores no Template | Enviar signals para pipelines de debounce e busca |
⚠️ 5. Armadilhas Comuns e Anti-Patterns
- Atraso de Microtask no
toObservable():- O
toObservable()não emite sincronicamente quando o signal é alterado; ele aguarda uma microtask do JavaScript para consolidar mudanças em lote (batching). Portanto, se você alterar um signal e imediatamente tentar ler a stream de forma síncrona, a nova emissão ainda não terá ocorrido.
- O
- Chamar
toSignal()sem Valor Inicial:- Sem
initialValue, o tipo inferido do signal seráSignal<T | undefined>. Isso força você a lidar comundefinedem todo o template ou código TypeScript.
- Sem
🛡️ 6. Boas Práticas e Arquitetura de Produção
- Evite o
AsyncPipeem Novos Códigos: Sempre que receber um Observable de um serviço, converta-o para signal comtoSignal()no componente. Isso simplifica a leitura no template (apenasmeuDado()), melhora a interoperabilidade comcomputed()e prepara seu projeto para o modo Zoneless. - Mantenha os Serviços Assíncronos em RxJS: Não force seus serviços a retornarem apenas Signals se envolverem streams contínuas ou canceláveis. Deixe a conversão para Signals na fronteira de consumo (no componente ou fachada).
📋 7. Cheat-Sheet: Resumo Executivo
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable ➔ Signal
const dados = toSignal(http.get('/api'), { initialValue: [] });
// Signal ➔ Observable
const termo$ = toObservable(this.termoSignal);