---
title: "Signals sem briga com RxJS"
description: "Uma fronteira de produção para Signals, computed, streams RxJS e interop, sem reescrever contratos Observable só para parecer atual."
deck: "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."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/signals-without-rxjs-war/"
lang: "pt-BR"
type: "article"
---

# 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](https://angular.dev/guide/signals) substituem [RxJS](https://rxjs.dev). 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](https://changelog.com/jsparty/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](https://angular.dev/guide/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`](https://angular.dev/api/core/rxjs-interop/toSignal) e [`toObservable`](https://angular.dev/api/core/rxjs-interop/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](https://blog.angular.dev/announcing-angular-v20-b5c9c06cf301), 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.

*Service com Observable, componente com Signal*

```ts
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
  );
}
```

*Entrada em Signal, tempo em RxJS*

```ts
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](https://github.com/angular/angular/discussions/49685)) colocou a distinção de forma concreta no [JS Party 310](https://changelog.com/jsparty/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`](https://angular.dev/api/core/effect) a ferramenta errada para manter dois Signals sincronizados.

[`effect`](https://angular.dev/api/core/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`](https://angular.dev/api/core/computed). Se a regra é tempo, prefira RxJS.

## Artefato reaproveitável: 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

## Leia também

- [Estado HTTP sem transformar tudo em Signals](https://andreramos.dev/pt/angular/http-state-without-signals-everywhere/)
- [NG0950: lendo um input obrigatório antes do Angular preencher](https://andreramos.dev/pt/angular/angular-ng0950-required-input-not-set/)
- [O que adotar, testar ou deixar para depois no Angular 21](https://andreramos.dev/pt/angular/adopt-spike-wait-angular-21/)
