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
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);
}
}
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) {}
}
Explicação Detalhada:
-
toSignal
converte o ObservablegetUsers
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 {}
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) {}
}
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));
}
}
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, aplicandodebounceTime
edistinctUntilChanged
para otimizar performance. - O Observable
users$
usamap
para filtrar usuários com base no Signalfilter
. -
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;
});
Template Atualizado:
<p>Mostrando {{ userCount() }} usuários. Comprimento médio do nome: {{ averageNameLength() | number:'1.2-2' }}</p>
Explicação:
-
userCount
eaverageNameLength
são Signals computados que derivam valores dofilteredUsers
. - 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
});
}
Cuidado: Use effect
com moderação, pois pode impactar o desempenho se mal utilizado.
Melhores Práticas para Usar RxJS com Signals
- Use Signals para Estado Local: Gerencie estado local de componentes (ex.: inputs de formulário, toggles) com Signals para simplicidade e performance.
- 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.
- Evite Sobrecarga de Observables: Se um estado pode ser gerenciado sincronamente com Signals, evite envolvê-lo em um Observable para reduzir complexidade.
-
Use
toSignal
com Sabedoria: ForneçainitialValue
para lidar com estados de carregamento de forma elegante. Considere opções comorequireSync
para cenários síncronos. -
Gerencie Subscrições: Ao usar
toSignal
, o Angular gerencia subscrições automaticamente. Para Observables personalizados, usetakeUntilDestroyed
de@angular/core/rxjs-interop
para limpeza. -
Otimize Signals Computados: Use
computed
para estado derivado, evitando cálculos redundantes e melhorando o desempenho. - Testabilidade: Signals facilitam testes unitários, pois são síncronos e previsíveis. Combine com mocks de RxJS para cenários assíncronos.
-
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)