Capítulo 10: Fundamentos do RxJS (Observables, Observers, Subscriptions)

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual da Anatomia do RxJS

flowchart TD
    Producer["Fonte Produtora de Eventos (HTTP / WebSocket / DOM)"] --> Observable["Observable (Stream Assíncrona no Tempo)"]

    Observable -->|"Inscrição: .subscribe(Observer)"| Observer["Observer (Consumidor)"]

    subgraph LIFECYCLE_NOTIF["Canais de Notificação do Observer"]
        N["next(valor) - Emissão de Dados"]
        E["error(err) - Emissão Terminal de Falha"]
        C["complete() - Emissão Terminal de Sucesso"]
    end

    Observer --- LIFECYCLE_NOTIF

    Observable -->|"Retorna Objeto de Controle"| Sub["Subscription"]
    Sub -->|".unsubscribe() / takeUntilDestroyed()"| MemorySafe["Liberação de Recursos (Prevenção de Memory Leaks)"]

    style Producer fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Observable fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style Observer fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style MemorySafe fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Paradigma da Programação Reativa com RxJS

Enquanto Signals são excelentes para representar o estado síncrono de uma aplicação, o RxJS (Reactive Extensions for JavaScript) é o padrão ouro absoluto para lidar com eventos assíncronos que ocorrem ao longo do tempo, como:

  • Requisições HTTP e retentativas (retries).
  • Fluxos de WebSockets contínuos.
  • Debounce de digitação do teclado e pesquisas em tempo real.
  • Orquestração de múltiplos eventos assíncronos paralelos ou sequenciais.

⚙️ 2. As Três Entidades Fundamentais: Observable, Observer e Subscription

2.1 O que é um Observable?

Um Observable é um fluxo preguiçoso (lazy) de valores ao longo do tempo. Ele não executa nenhuma ação até que algum consumidor se inscreva nele via .subscribe().

import { Observable } from 'rxjs';

// Criação manual de um Observable para entender seu funcionamento interno
const streamTempo$ = new Observable<string>(subscriber => {
  console.log('Iniciando produção de dados...');
  
  subscriber.next('Primeiro evento');
  subscriber.next('Segundo evento');

  const timer = setTimeout(() => {
    subscriber.next('Terceiro evento (após 1 segundo)');
    subscriber.complete(); // Finaliza a stream
  }, 1000);

  // Função de limpeza (Tear-down logic) executada no unsubscribe
  return () => {
    clearTimeout(timer);
    console.log('Stream encerrada e recursos liberados!');
  };
});

2.2 O Observer e os 3 Canais de Notificação

O Observer é o objeto que consome os dados do Observable através de 3 callbacks:

  1. next: (dado) => void: Invocado zero ou infinitas vezes quando um novo dado chega.
  2. error: (err) => void: Notificação terminal disparada caso ocorra uma falha. Encerra a stream.
  3. complete: () => void: Notificação terminal disparada quando a stream encerra com sucesso.
// Consumindo a stream com um Observer completo
const subscricao = streamTempo$.subscribe({
  next: (valor) => console.log('Dado recebido:', valor),
  error: (erro) => console.error('Erro na stream:', erro),
  complete: () => console.log('Stream finalizada com sucesso!')
});

// Cancelamento antecipado
// subscricao.unsubscribe();

❄️ vs 🔥 3. Cold vs Hot Observables e a Família Subject

3.1 Cold Observables

  • O produtor é criado dentro do Observable para cada novo assinante.
  • Cada assinante recebe sua própria execução isolada.
  • Exemplo típico: HttpClient.get() do Angular. Se dois componentes fizerem .subscribe(), duas requisições HTTP distintas serão enviadas à API.

3.2 Hot Observables e Subjects

  • O produtor existe de forma independente antes mesmo de haver qualquer assinante.
  • Múltiplos assinantes compartilham a mesma execução (multicasting).
  • Tipos de Subjects:
    1. Subject<T>: Não tem valor inicial nem memória. Novos assinantes só recebem dados emitidos após o momento de sua inscrição.
    2. BehaviorSubject<T>: Requer um valor inicial. Emite imediatamente seu valor atual mais recente para qualquer novo assinante.
    3. ReplaySubject<T>: Armazena um buffer com os últimos $N$ valores e os reproduz para novos inscritos.
import { BehaviorSubject } from 'rxjs';

// BehaviorSubject armazena o último estado conhecido
const usuarioAuth$ = new BehaviorSubject<{ id: number; nome: string } | null>(null);

// Assinante 1 recebe null imediatamente
usuarioAuth$.subscribe(u => console.log('Assinante 1:', u));

// Emite novo valor para todos os inscritos
usuarioAuth$.next({ id: 101, nome: 'Mariana Silva' });

// Assinante 2 se inscreve depois e recebe imediatamente o usuário atual (Mariana)
usuarioAuth$.subscribe(u => console.log('Assinante 2:', u));

🛡️ 4. Gestão Automática de Desinscrição com takeUntilDestroyed

Em aplicações Angular, inscrições manuais (.subscribe()) que nunca são canceladas permanecem vivas na memória do browser mesmo após a destruição do componente, gerando os famosos Memory Leaks.

No Angular 16+ e consolidado no 18+, utiliza-se o operador nativo takeUntilDestroyed:

import { Component, inject } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-monitor-pulsos',
  standalone: true,
  template: `<p>Monitorando pulsos em tempo real...</p>`
})
export class MonitorPulsosComponent {
  constructor() {
    // takeUntilDestroyed amarra o ciclo de vida do Observable à destruição deste componente!
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe(tick => {
        console.log(`Pulso recebido: ${tick}`);
      });
  }
}

⚖️ 5. Comparativo: Signals vs Observables (RxJS)

Dimensão Técnica Signals (signal()) Observables (Observable$)
Foco Principal Estado de UI síncrono e valores correntes Fluxos assíncronos e eventos no tempo
Inscrição Obrigatória Não (lido de forma síncrona valor()) Sim (requer .subscribe() ou AsyncPipe)
Risco de Memory Leak Praticamente zero Alto se não houver cancelamento
Operadores Complexos Não possui (focado em simplicidade) Centenas (retry, switchMap, debounce)
Cancelamento de Ações Não nativo Suporte nativo a cancelamento e abort

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Assinaturas Aninhadas (Nested Subscribes):
    • Anti-Pattern: Fazer this.http.get().subscribe(user => { this.http.getPost(user.id).subscribe(...) }).
    • Correção: Utilize operadores de achatamento (Higher-Order Mapping Operators) como switchMap ou concatMap (estudados no próximo capítulo).
  2. Esquecer de Cancelar Subscrições Infinitas:
    • Observables de timers (interval), WebSockets ou eventos de DOM nunca completam sozinhos. Sempre use takeUntilDestroyed() ou converta para signal com toSignal().

📋 7. Cheat-Sheet: Síntese de RxJS Básico

// Observable Cold manual
const obs$ = new Observable(sub => { sub.next(1); sub.complete(); });

// Assinatura segura no ciclo de vida
obs$.pipe(takeUntilDestroyed()).subscribe({
  next: val => console.log(val),
  error: err => console.error(err)
});

// Subject com estado inicial
const state$ = new BehaviorSubject<number>(0);
state$.next(10);