DEV Community

Cover image for Usando RxJS com Signals no Angular: Uma Abordagem Moderna para Gerenciamento de Estado Reativo
Kauê Matos
Kauê Matos

Posted on

Usando RxJS com Signals no Angular: Uma Abordagem Moderna para Gerenciamento de Estado Reativo

O Angular evoluiu significativamente ao longo dos anos, e com a introdução dos Signals no Angular 16, os desenvolvedores ganharam uma ferramenta poderosa para gerenciar estado reativo de forma mais simples e intuitiva. Quando combinados com o RxJS, os Signals do Angular permitem uma reatividade fina e uma integração perfeita com fluxos de dados assíncronos. Este artigo explora em detalhes como usar efetivamente o RxJS com os Signals do Angular, fornecendo exemplos práticos, melhores práticas e considerações avançadas para aplicações Angular modernas.

O Que São Signals no Angular?

Os Signals são primitivas reativas introduzidas para gerenciar mudanças de estado em aplicações Angular. Um Signal é um wrapper de valor que notifica os consumidores quando o valor muda, permitindo uma reatividade de grão fino. Os Signals são particularmente úteis para gerenciar estado de maneira previsível e performática, pois o Angular pode otimizar a detecção de mudanças rastreando apenas as partes específicas da aplicação afetadas pelas alterações de estado.

Principais Recursos dos Signals:

  • Reatividade: Notifica automaticamente componentes dependentes ou computações quando o valor muda.
  • Grão Fino: Atualiza apenas as partes da UI afetadas pela mudança de estado, reduzindo re-renderizações desnecessárias.
  • API Simples: Fácil de ler e escrever em comparação com construções tradicionais de programação reativa.
  • Integração Nativa: Funciona perfeitamente com o sistema de detecção de mudanças do Angular, eliminando a necessidade de ChangeDetectorRef em muitos casos.

Os Signals são compostos por três principais funções:

  • signal(value): Cria um Signal writable com um valor inicial.
  • computed(fn): Cria um Signal derivado que computa seu valor com base em outros Signals.
  • effect(fn): Executa uma função de efeito colateral sempre que Signals dependentes mudam.

RxJS: A Base para Operações Assíncronas no Angular

O RxJS é uma biblioteca para programação reativa usando Observables, ideal para lidar com operações assíncronas como requisições HTTP, entradas de usuário ou timers. Os Observables fornecem uma maneira poderosa de compor e transformar fluxos de dados, mas podem ser complexos para tarefas simples de gerenciamento de estado.

Ao combinar Observables do RxJS com Signals do Angular, os desenvolvedores podem aproveitar as forças de ambos: as capacidades robustas assíncronas do RxJS e a reatividade leve e síncrona dos Signals.

Por Que Combinar RxJS e Signals?

Enquanto os Signals são excelentes para gerenciar estado local e síncrono, o RxJS brilha no manuseio de operações assíncronas e transformações complexas de dados. A combinação permite:

  • Converter fluxos de dados assíncronos (ex.: respostas HTTP) em Signals para atualizações reativas na UI.
  • Simplificar o gerenciamento de estado usando Signals para estado local de componentes e RxJS para dados externos.
  • Otimizar o desempenho minimizando ciclos de detecção de mudanças com a reatividade de grão fino dos Signals.
  • Evitar vazamentos de memória comuns em subscrições de Observables, pois os Signals gerenciam dependências automaticamente.

Configurando o Ambiente

Para seguir os exemplos, certifique-se de ter um projeto Angular com versão 16 ou superior e RxJS instalado. Se estiver começando do zero, crie um novo projeto:

ng new demo-rxjs-signals
cd demo-rxjs-signals
npm install
Enter fullscreen mode Exit fullscreen mode

O Angular inclui o RxJS por padrão, então não é necessário instalação adicional.

Ative os Signals adicionando @angular/core e importando as funções necessárias.

Convertendo Observables do RxJS para Signals

Um dos casos de uso mais comuns é converter um Observable do RxJS em um Signal para uso em templates ou propriedades computadas. O Angular fornece a função toSignal do pacote @angular/core/rxjs-interop para isso.

Exemplo: Buscando Dados com HTTP e Signals

Vamos criar um exemplo simples onde buscamos uma lista de usuários de uma API e os exibimos usando Signals.

Passo 1: Crie um Serviço para Requisições HTTP

Crie um serviço para lidar com requisições HTTP usando RxJS.

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Passo 2: Converta o Observable para Signal em um Componente

No componente, use toSignal para converter o Observable em um Signal.

// user.component.ts
import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';

@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="users(); else loading">
      <ul>
        <li *ngFor="let user of users()">{{ user.name }}</li>
      </ul>
    </div>
    <ng-template #loading>Carregando...</ng-template>
  `,
  standalone: true,
})
export class UserComponent {
  users = toSignal(this.userService.getUsers(), { initialValue: [] });

  constructor(private userService: UserService) {}
}
Enter fullscreen mode Exit fullscreen mode

Explicação Detalhada:

  • toSignal converte o Observable getUsers em um Signal.
  • A opção initialValue fornece um valor padrão ([]) enquanto a requisição HTTP está pendente, evitando erros na renderização inicial.
  • No template, users() acessa o valor do Signal de forma reativa, atualizando automaticamente a UI quando os dados chegam.
  • Isso elimina a necessidade de async pipe, que pode ser menos eficiente em cenários de alta frequência de atualizações.

Passo 3: Adicione o Componente à Sua Aplicação

Declare o componente no módulo da app ou em uma aplicação standalone.

// app.component.ts
import { Component } from '@angular/core';
import { UserComponent } from './user/user.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [UserComponent],
  template: `<app-user></app-user>`,
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

Execute a aplicação com ng serve e veja a lista de usuários sendo carregada reativamente.

Tratando Erros e Estados de Carregamento

Para um gerenciamento mais robusto, adicione estados de erro e carregamento usando Signals compostos.

// user.component.ts (versão estendida)
import { Component, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';
import { UserService } from './user.service';

@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="loading(); else content">Carregando...</div>
    <ng-template #content>
      <div *ngIf="error(); else usersList">{{ error() }}</div>
      <ng-template #usersList>
        <ul>
          <li *ngFor="let user of users()">{{ user.name }}</li>
        </ul>
      </ng-template>
    </ng-template>
  `,
  standalone: true,
})
export class UserComponent {
  private users$ = this.userService.getUsers().pipe(
    catchError(err => of({ error: 'Erro ao carregar usuários: ' + err.message }))
  );
  private rawUsers = toSignal(this.users$, { initialValue: [] });
  loading = computed(() => this.rawUsers().length === 0 && !this.error());
  error = computed(() => typeof this.rawUsers() === 'object' && 'error' in this.rawUsers() ? this.rawUsers().error : null);
  users = computed(() => Array.isArray(this.rawUsers()) ? this.rawUsers() : []);

  constructor(private userService: UserService) {}
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • Usamos catchError do RxJS para capturar erros na requisição.
  • Signals computados (loading, error, users) derivam estados da resposta, tornando o código declarativo e fácil de manter.

Combinando Signals com Operadores do RxJS

Você pode aprimorar a integração usando operadores do RxJS para transformar o Observable antes de convertê-lo em Signal. Por exemplo, filtre usuários cujos nomes começam com uma letra específica.

Exemplo: Filtragem Reativa com Input do Usuário

// user.component.ts
import { Component, signal, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { UserService } from './user.service';

@Component({
  selector: 'app-user',
  template: `
    <input #filterInput type="text" placeholder="Filtrar por nome" />
    <div *ngIf="filteredUsers(); else loading">
      <ul>
        <li *ngFor="let user of filteredUsers()">{{ user.name }}</li>
      </ul>
    </div>
    <ng-template #loading>Carregando...</ng-template>
  `,
  standalone: true,
})
export class UserComponent {
  filter = signal('');
  private input$ = fromEvent<HTMLInputElement>(this.filterInput.nativeElement, 'input').pipe(
    map(event => (event.target as HTMLInputElement).value),
    debounceTime(300),
    distinctUntilChanged()
  );
  users$ = this.userService.getUsers().pipe(
    map(users =>
      users.filter(user =>
        user.name.toLowerCase().startsWith(this.filter().toLowerCase())
      )
    )
  );
  filteredUsers = toSignal(this.users$, { initialValue: [] });

  constructor(private userService: UserService) {
    this.input$.subscribe(value => this.filter.set(value));
  }
}
Enter fullscreen mode Exit fullscreen mode

Explicação Detalhada:

  • Um Signal filter captura a entrada do usuário.
  • Usamos fromEvent do RxJS para criar um Observable a partir do evento de input, aplicando debounceTime e distinctUntilChanged para otimizar performance.
  • O Observable users$ usa map para filtrar usuários com base no Signal filter.
  • toSignal converte o Observable transformado em um Signal (filteredUsers).
  • A template atualiza reativamente à medida que o usuário digita, filtrando a lista em tempo real sem sobrecarregar o servidor.

Signals Computados com RxJS

Use a função computed dos Signals para derivar novo estado a partir de Signals existentes, combinando-os com Observables do RxJS para lógicas mais complexas.

Exemplo: Contando Usuários Filtrados e Estatísticas

Adicione Signals computados para exibir o número de usuários filtrados e estatísticas adicionais.

// user.component.ts (estendido)
... // Código anterior

userCount = computed(() => this.filteredUsers().length);
averageNameLength = computed(() => {
  const users = this.filteredUsers();
  return users.length > 0 ? users.reduce((sum, u) => sum + u.name.length, 0) / users.length : 0;
});
Enter fullscreen mode Exit fullscreen mode

Template Atualizado:

<p>Mostrando {{ userCount() }} usuários. Comprimento médio do nome: {{ averageNameLength() | number:'1.2-2' }}</p>
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • userCount e averageNameLength são Signals computados que derivam valores do filteredUsers.
  • Eles atualizam automaticamente quando o filtro muda, sem necessidade de subscrições manuais.
  • Isso demonstra como os Signals facilitam computações derivadas, integrando-se perfeitamente com dados de RxJS.

Usando Effects com RxJS

Os Effects executam código colateral quando Signals mudam. Combine com RxJS para ações assíncronas, como logging ou chamadas adicionais.

Exemplo: Logging de Mudanças

// user.component.ts
import { effect } from '@angular/core';

constructor(private userService: UserService) {
  effect(() => {
    console.log(`Filtro atualizado para: ${this.filter()}`);
    // Poderia disparar um Observable aqui para ações assíncronas
  });
}
Enter fullscreen mode Exit fullscreen mode

Cuidado: Use effect com moderação, pois pode impactar o desempenho se mal utilizado.

Melhores Práticas para Usar RxJS com Signals

  1. Use Signals para Estado Local: Gerencie estado local de componentes (ex.: inputs de formulário, toggles) com Signals para simplicidade e performance.
  2. Use RxJS para Operações Assíncronas: Lide com requisições HTTP, timers ou fluxos de eventos com Observables do RxJS, convertendo para Signals apenas quando necessário para a UI.
  3. Evite Sobrecarga de Observables: Se um estado pode ser gerenciado sincronamente com Signals, evite envolvê-lo em um Observable para reduzir complexidade.
  4. Use toSignal com Sabedoria: Forneça initialValue para lidar com estados de carregamento de forma elegante. Considere opções como requireSync para cenários síncronos.
  5. Gerencie Subscrições: Ao usar toSignal, o Angular gerencia subscrições automaticamente. Para Observables personalizados, use takeUntilDestroyed de @angular/core/rxjs-interop para limpeza.
  6. Otimize Signals Computados: Use computed para estado derivado, evitando cálculos redundantes e melhorando o desempenho.
  7. Testabilidade: Signals facilitam testes unitários, pois são síncronos e previsíveis. Combine com mocks de RxJS para cenários assíncronos.
  8. Migração de Código Legado: Converta gradualmente pipes async para Signals para melhorar a performance em aplicações existentes.

Considerações de Desempenho

  • Signals Reduzem Sobrecarga de Detecção de Mudanças: Diferente de Observables com async pipe, os Signals não acionam detecção global de mudanças, tornando-os mais eficientes para atualizações de UI.
  • Minimize Subscrições Ativas: Converta Observables para Signals cedo no fluxo de dados para reduzir o número de subscrições.
  • Evite Ciclos em Effects: Certifique-se de que effects não modifiquem Signals que os acionem, para evitar loops infinitos.
  • Perfil de Desempenho: Use ferramentas como o Angular DevTools para monitorar atualizações de Signals e otimizar gargalos.

Considerações Avançadas

  • Integração com NgRx ou Outros Stores: Use Signals para estado local e RxJS para fluxos globais em stores como NgRx, convertendo Effects para Signals onde possível.
  • Reatividade em Serviços: Crie serviços que expõem Signals derivados de Observables para compartilhamento de estado entre componentes.
  • Atualizações no Angular: Com versões futuras (como Angular 17+), espere melhorias na interoperabilidade, como suporte nativo para mais operadores RxJS em Signals.
  • Limitações: Signals são síncronos por natureza; para fluxos assíncronos complexos, RxJS permanece essencial. Evite misturar excessivamente para manter a clareza do código.

Conclusão

A combinação de RxJS com Signals no Angular oferece o melhor dos dois mundos: o poder do RxJS para lidar com fluxos de dados assíncronos complexos e a simplicidade dos Signals para gerenciamento de estado reativo. Usando toSignal para conectar Observables e Signals, e computed para estado derivado, os desenvolvedores podem construir aplicações Angular performáticas, reativas e com código mais limpo e manutenível.

À medida que o Angular continua a evoluir, a sinergia entre RxJS e *Signals * provavelmente se tornará um pilar do desenvolvimento Angular moderno. Experimente essas ferramentas em seus projetos para desbloquear todo o seu potencial!

Top comments (0)