10 operadores do RxJS indispensáveis para sua App Angular
O RxJS é uma biblioteca para programação reativa em JavaScript que se tornou uma parte fundamental do desenvolvimento de aplicações Angular. Ela simplifica a manipulação de fluxos de dados assíncronos, eventos e chamadas de API, tornando o código mais conciso e fácil de entender.
Neste artigo, exploraremos 10 operadores do RxJS que são indispensáveis para potencializar a eficiência e a manutenção de sua aplicação Angular. Mas antes disso, vamos entender o motivo deles serem tão úteis.
Utilize mais os operadores e menos o subscribe
É comum no início do aprendizado do RxJS, utilizar somente o básico de Observables. Por exemplo, fazer uma requisição HTTP ou ouvir mudanças de um formulário utilizando o método subscribe
e manipular toda a lógica dentro dele. Porém, conforme vamos aprofundando nossos estudos no RxJS, entendemos que existem maneiras mais eficientes de realizar estas operações com os diversos operadores disponíveis.
Deixar a lógica dentro dos operadores em vez de dentro do bloco subscribe
tem a ver com a natureza da programação reativa e os benefícios que ela proporciona. Listo abaixo algumas razões pelas quais é geralmente considerado uma prática melhor:
- Composição e Legibilidade: O uso de operadores permite a composição de fluxos de dados de forma mais legível e concisa. Cada operador representa uma etapa específica na transformação ou manipulação dos dados. Isso facilita a leitura e compreensão do fluxo, especialmente quando há várias transformações a serem aplicadas.
- Encadeamento de Operadores: Os operadores podem ser encadeados para formar uma sequência de transformações, filtragens e manipulações. Isso resulta em um código mais linear, evitando o famoso callback hell que pode ocorrer quando a lógica está aninhada dentro de blocos
subscribe
. - Reusabilidade: Ao encapsular a lógica em operadores, você pode reutilizar esses operadores em diferentes partes do código ou até mesmo em projetos diferentes. Isso promove a modularição e reduz a duplicação de código.
- Manutenibilidade: Separar a lógica do
subscribe
torna o código mais fácil de manter. Se você precisar fazer alterações ou adicionar novos comportamentos, pode fazê-lo diretamente nos operadores, sem interferir na lógica de subscrição ou em outras partes do código. - Controle de erros: Muitos operadores oferecem maneiras eficientes de lidar com erros, como o operador
catchError
. Tratar erros dentro dos operadores é mais limpo e eficaz do que fazer isso dentro do blocosubscribe
, onde o código pode ficar rapidamente poluído com manipulação de erros. - Desacoplamento: Colocar lógica dentro do
subscribe
pode resultar em um acoplamento mais forte entre a obtenção de dados e sua manipulação. Operadores permitem desacoplar essas duas preocupações, facilitando a modificação ou substituição de operadores sem afetar a lógica de subscrição. - Facilita a testabilidade: A lógica contida em operadores pode ser mais facilmente testada, pois muitos frameworks de teste oferecem suporte direto à criação e manipulação de observables. Isso facilita a criação de casos de teste específicos para cada operador, garantindo a robustez do código.
Exemplo de uso
Agora que entendemos todas as suas vantagens, nada melhor do que listar os operadores com um exemplo prático. Vamos utilizar o código de exemplo de um Typeahead. O Typeahead é um termo comumente utilizado para descrever uma funcionalidade que fornece sugestões automáticas enquanto o usuário está digitando em um campo de input de texto.
Este é um dos melhores exemplos para demonstrar todo o poder do RxJS e seus operadores. Realizar o mesmo comportamento sem ele, seria um grande desafio. A chance de entregar um código nada performático e com bugs seria bem maior.
Segue abaixo o código da implementação e a explicação da responsabilidade de cada um dos operadores.
@Component({
selector: 'app-root',
template: `
<input type="search" [formControl]="control" />
<ul>
<li *ngFor="let result of results$ | async">{{result.name}}</li>
</ul>
`,
})
export class AppComponent {
constructor(private http: HttpClient) {}
control = new FormControl<string>('', { nonNullable: true });
results$ = this.control.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter((search) => search.length > 1),
switchMap((search) =>
this.http.get<any>(`https://swapi.dev/api/people/?search=${search}`)
),
map((search) => search.results),
startWith([]),
tap((result) => console.log(result)),
retry(2),
catchError(error => {
console.error(error);
return of([])
})
);
}
1. debounceTime()
Para lidar com eventos de digitação, como inputs de pesquisa, o debounceTime()
é fundamental. Ele impede a emissão de eventos por um determinado período, reduzindo a frequência de chamadas desnecessárias.
Passamos via parâmetro um período de 300 milissegundos, evitando assim consultas desnecessárias na API dentro deste intervalo de tempo.
2. distinctUntilChanged()
O operador distinctUntilChanged()
impede que valores duplicados sejam emitidos consecutivamente. Isso é valioso para evitar atualizações desnecessárias.
No nosso exemplo, caso o usuário informe um texto exatamente igual o anterior, o observable vai ignorar essa emissão pois seria uma consulta desnecessária na API.
3. filter()
O operador filter()
é útil para filtrar os itens de um fluxo com base em uma condição. Isso é muito útil ao lidar com grandes conjuntos de dados, economizando recursos ao transmitir apenas os dados desejados.
No typeahead, estamos validando somente pesquisas onde o termo buscado tenha no mínimo 2 caracteres. Fazer uma busca com nenhum ou somente um caractere pode não ser interessante.
4. switchMap()
O operador switchMap()
é usado para mapear os valores de um observable para outro observable, mas ao contrário do operador mergeMap
, o switchMap
cancela a subscrição anterior sempre que um novo valor é emitido. Isso significa que apenas o observable resultante da última emissão será mantido ativo.
A principal utilidade do switchMap
está em cenários em que você tem observables aninhados e deseja garantir que apenas o observable mais recente permaneça ativo. Isso é particularmente útil em situações em que há uma alta frequência de emissões e você está interessado apenas nos resultados mais recentes.
É exatamente o propósito do nosso typeahead. Se o usuário realizou várias consultas e a API ainda não entregou a resposta, o operador automaticamente cancela as requisições anteriores.
5. map()
O operador map()
é essencial para transformar os valores de um fluxo. Ele permite que você aplique uma função a cada item emitido, modificando os dados de acordo com suas necessidades. Na prática, é comumente utilizado para mapear os resultados de chamadas de API.
No nosso caso, a API retorna um objeto com várias informações, mas estamos interessados somente no array results
que contém os resultados encontrados da busca.
6. startWith()
O operador startWith()
é utilizado para emitir um valor inicial imediatamente no início de um observable. Ele é especialmente útil quando você deseja garantir que um valor específico seja emitido antes mesmo de o observable começar a emitir os valores propriamente ditos.
Antes do usuário digitar algo no input de texto, já estamos retornando um array vazio. Então essa é a primeira emissão do observable.
7. tap()
O operador tap()
é usado para realizar ações secundárias sem afetar o fluxo principal. Os famos side effects. Pode ser utilizado para depuração, logging ou qualquer ação necessária.
Depois que fizemos a consulta na API e mapeamos os resultados, estamos dando um console.log
no valor atual que está sendo passado no fluxo.
8. retry()
O retry()
é útil para lidar com situações temporárias de falha em chamadas de API. Ele reinicia o fluxo em caso de erro, dando à aplicação a oportunidade de se recuperar automaticamente.
Passamos via parâmetro o número 2. Ou seja, instruímos o observable a fazer mais 2 tentativas antes de retornar algum erro.
9. catchError()
Para lidar com erros em observbles, o catchError()
é essencial. Ele permite que você trate erros sem quebrar o fluxo de dados, possibilitando a recuperação ou o tratamento adequado.
Em nosso exemplo, se mesmo depois das tentativas com o operador retry()
a API continuar retornando erro, então o código que está dentro deste operador é executado.
10. of()
O of()
não é exatamente um operador, mas sim uma função que cria um observable a partir de uma sequência fixa de valores. Ele é comumente utilizado para criar observables com um conjunto predefinido de valores, seja um único valor ou uma sequência de valores.
Estamos tratando os erros do nosso observable pelo operador catchError()
. Para que o fluxo prossiga, precisamos retornar um outro observable com algum valor, desta forma prevenimos que a aplicação não se comporte de maneira inesperada. Portanto, depois de tratar o erro, retornamos um observable com um array vazio.
Conclusão
Ao dominar esses 10 operadores do RxJS, você estará mais bem equipado para enfrentar desafios complexos em suas aplicações Angular.
Existem muitos outros operadores úteis que podem ser explorados via documentação oficial. Uma dica para descobrir os operadores perfeitos para cada cenário é utilizar o Operator Decision Tree, onde você responde algumas perguntas e no final é recomendado o operador ideal para a sua necessidade.
O código final da implementação pode ser conferido pelo StackBlitz: