Modern Angular
Signals sem briga com RxJS
Signals são ótimos para estado atual e derivação síncrona, e ficaram estáveis no Angular v20 (maio/2025). RxJS continua sendo a melhor linguagem para debounce, cancelamento, polling, retry e eventos ao longo do tempo. A fronteira é a decisão de design; o resto é consequência.
A pergunta errada
A versão menos útil dessa conversa é perguntar se Signals substituem RxJS. Isso vira guerra de ferramenta e leva times a reescrever contratos Observable ou forçar fluxos assíncronos em Signals só para parecer atual.
A pergunta que importa é: que tipo de problema esse estado resolve? Se é um valor atual ou uma derivação síncrona, Signals normalmente encaixam melhor. Se é um fluxo de eventos com debounce, cancelamento, retry, polling ou WebSocket, RxJS ainda é a ferramenta que eu quero usar.
Alex Rickabaugh (Angular team) traçou a mesma linha no JS Party episódio 310: 'Signals are values that can change over time, and RxJS observables really give you notifications of events happening at a specific point in time.' Ou seja, signals representam valor que muda, observables representam eventos no tempo. O próprio time do framework ensina os dois como primitivas diferentes, não como competidores.
A divisão que eu uso
| Situação | Escolha padrão | Motivo |
|---|---|---|
| Aba ativa, texto de filtro, linha expandida | Signal | Estado local de UI com um valor atual. |
| Label derivada, flag de permissão, contagem filtrada | computed | Derivação síncrona a partir de outros valores. |
| Service existente com user$ ou permissions$ | toSignal na fronteira do componente | Mantém o contrato do service e simplifica leitura no template. |
| Autocomplete com debounce e cancelamento | Pipeline RxJS mais Signal para estado de UI | Debounce e cancelamento pertencem ao stream. |
| NgRx ou ComponentStore coordenando uma feature | Ambos | Mantém a espinha dorsal da store e expõe Signals onde ajudam o consumo. |
A fronteira que eu gosto
Uma política útil para o time precisa caber em code review: use Signals para estado local e derivado de componente; mantenha RxJS para streams, composição assíncrona, eventos no tempo, integração com NgRx e fluxos com cancelamento.
Interop vale a pena quando deixa a fronteira explícita via toSignal e toObservable. Não é uma imposição pra converter o app inteiro. Um componente pode ler um Signal user derivado de user$ enquanto o service de autenticação continua expondo Observables porque esse ainda é o contrato certo para o resto do app.
Quando o Angular team comunica adoção, tende a comunicar com dado de produção. No anúncio do v20, o YouTube reportou 35% de melhoria em interaction latency no Living Room ao migrar para Angular Signals via Wiz. Esse tipo de resultado aparece quando o time trata Signals como a primitiva certa para estado de UI, não quando tenta readaptar estado Observable em forma de Signal.
No exemplo de busca, normalize a query antes de distinctUntilChanged(). Caso contrário, angular e angular viram requests diferentes, e o stream faz trabalho evitável.
export class HeaderComponent {
private readonly auth = inject(AuthService);
readonly user = toSignal(this.auth.user$, { initialValue: null });
readonly canAdmin = computed(() =>
this.user()?.roles.includes('admin') ?? false
);
}private readonly api = inject(SearchApi);
readonly query = signal('');
private readonly query$ = toObservable(this.query);
readonly results = toSignal(
this.query$.pipe(
map(query => query.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(term => {
if (!term) return of([]);
return this.api.search(term).pipe(
catchError(() => of([]))
);
})
),
{ initialValue: [] }
);A armadilha
Pawel Kozlowski (Angular team, autor da RFC Signals) colocou a distinção de forma concreta no JS Party 310: 'A signal — I can always look at this box, and I'm going to get the value. If I subscribe to an event emitter, there was no event fired, I don't have a value.' Ou seja, signal sempre tem valor atual disponível; observable só notifica quando o evento acontece. Essa distinção é o que torna effect a ferramenta errada para manter dois Signals sincronizados.
effect não é o novo subscribe para tudo. O cheiro ruim é um effect que existe só para manter outro Signal sincronizado. Use quando estado reativo precisa tocar um sistema externo: localStorage, analytics, canvas ou biblioteca imperativa. Se a regra é derivação, prefira computed. Se a regra é tempo, prefira RxJS.
Artefato para reaproveitar
Regra de fronteira Signals/RxJS
- Valor atual: Signal.
- Valor derivado atual: computed.
- Eventos no tempo: RxJS.
- Observable legado consumido por componente: toSignal na fronteira do componente.
- Efeito externo a partir de estado reativo: effect, com parcimônia.
Fontes consultadas
- https://angular.dev/guide/signals
- https://angular.dev/api/core/signal
- https://angular.dev/api/core/computed
- https://angular.dev/api/core/effect
- https://angular.dev/api/core/rxjs-interop/toSignal
- https://angular.dev/api/core/rxjs-interop/toObservable
- https://angular.dev/api/core/rxjs-interop/rxResource
- https://rxjs.dev/api/index/class/BehaviorSubject
- https://github.com/angular/angular/discussions/49685
- https://github.com/angular/angular/discussions/60121
- https://blog.angular.dev/announcing-angular-v20-b5c9c06cf301
- https://changelog.com/jsparty/310
Modern Angular Playbook
Este artigo é um play.
O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.
Você recebe os dois PDFs por email, pela lista do Dojo IA.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →