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çãoEscolha padrãoMotivo
Aba ativa, texto de filtro, linha expandidaSignalEstado local de UI com um valor atual.
Label derivada, flag de permissão, contagem filtradacomputedDerivação síncrona a partir de outros valores.
Service existente com user$ ou permissions$toSignal na fronteira do componenteMantém o contrato do service e simplifica leitura no template.
Autocomplete com debounce e cancelamentoPipeline RxJS mais Signal para estado de UIDebounce e cancelamento pertencem ao stream.
NgRx ou ComponentStore coordenando uma featureAmbosManté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.

Service com Observable, componente com Signalts
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 RxJSts
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

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.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Guia · 9 min

Estado HTTP sem transformar tudo em Signals

Um guia técnico para fronteiras de estado HTTP no Angular com HttpClient, RxJS, toSignal e experimentos cautelosos com httpResource.

Ler artigo →

Erro · 7 min

NG0950: lendo um input obrigatório antes do Angular preencher

Por que o NG0950 do Angular dispara quando um signal input obrigatório é lido antes do binding, quando os inputs ficam disponíveis de fato, e os três consertos: ler no ngOnInit / computed / effect, dar um valor padrão, ou fazer o binding no pai.

Ler artigo →

Nota · 6 min

O que adotar, testar ou deixar para depois no Angular 21

Um filtro de adoção para Angular 21: o que entra em trechos que já vão mudar, o que merece branch e o que deve ficar longe de fluxos críticos.

Ler artigo →