DEV Community

Edward Teixeira Dias Junior
Edward Teixeira Dias Junior

Posted on

RXJS parte 1 - Observables

Introdução

Esse é o primeiro de uma série de artigos sobre RXJS, onde aprenderemos os mais diversos operadores e suas aplicabilidades através de pequenos projetos que usaremos como exemplos.

Nesse primeiro post, vamos introduzir o conceito de observables, conhecimento indispensável para dominar tudo que a programação reativa nos tem a nos oferecer.

Contexto

Antes de partimos para as definições em sí, vamos contextualizar os motivos de se usar observables. Considere o código abaixo:

// Aqui, declaramos uma variável e atribuímos o valor de 10, portanto x = 10.
let x = 10;
Enter fullscreen mode Exit fullscreen mode
// A mesma coisa com arrays
let array = [1, 2, 3];
Enter fullscreen mode Exit fullscreen mode

Depois de declaradas, variáveis podem ser usadas durante todo o programa, para os mais diversos casos de uso. Contudo, a vida seria muito simples se todas as aplicações ou programas se comportassem dessa forma. Nem sempre temos todos os dados de que precisamos de forma instantânea quando o nosso programa ou aplicação forem inicializados.Ás vezes precisamos fazer requisições AJAX que trarão (ou não, caso ocorra algum erro) esses dados de que precisamos em algum momento, que não sabemos de antemão.

Por exemplo, digamos que queremos informações de algum usuário para executar alguma funcionalidade no nosso sistema:

// Variável que espera o retorno de uma requisição AJAX
const userRequest = pegarDadosDoUsuário();
// Função para fazer algum processamento com os dados recebidos
facaAlgumaCoisa(userRequest);
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, duas coisas podem acontecer: um enxurrada de erros no nosso sistema e/ou um print hemorrágico da stackTrace. Ora, estamos declarando uma variável, atribuindo um valor de um retorno de uma função que nem sabemos quando vamos ter acesso e passando essa variável para uma outra função que irá fazer algum processamento com base nesse dado que pode nem estar disponível. Dá pra imaginar? Bom, podemos consertar o nosso programa fazendo com que a nossa função facaAlgumaCoisa() só seja invocada quando tivermos algum dado na nossa variável user. Parece bom, certo?

Promises

Uma promise representa uma promessa de que os dados solicitados vão estar disponíveis em algum momento no futuro. Quando esses dados chegarem, devemos acessá-los usando um método chamado .then( ), assim podemos ter de fato acessar os nossos dados de que tanto precisamos e continuar o fluxo normal do nosso sistema.

// Variável que espera o retorno de uma requisição AJAX que retorna uma promise
const userRequest = pegarDadosDoUsuário();

// Chamada do método then para termos acesso aos dados de user
userRequest.then((dadosDoUsuarioVindosDoServidor) => {
  // Função para fazer algum processamento com os dados de user
  facaAlgumaCoisa(dadosDoUsuarioVindosDoServidor);
});
Enter fullscreen mode Exit fullscreen mode

O comportamente de uma promise faz jus ao nome, ou seja, é realmente uma promessa de que dados irão retornar de uma requisição e enquanto esses dados não voltam, processos são liberados para fazer outras coisas até o nosso servidor percorrer os vários índices do nosso banco de dados e finalmente mandar a nossa resposta. Uma vez que a reposta finalmente chega do nosso servidor, a promise é realizada com sucesso e acessamos o método then( ) para recebermos o nossos tão aguardados dados - e daí em diante finalmente podemos seguir em paz com o nosso programa fazendo uso dessa variável.

Lembrando que caso essa função retorne algum erro, podemos tratar com um outro método chamado catch( ), tendo assim acesso ao o que aconteceu com a nossa requisição através da variável error.

É com esse problema de natureza assíncrona que observables se tornam uma boa ferramenta para serem utilizadas em nossas aplicações.

Observables

Observables são como arrays, e representam uma coleção de eventos, e também são como promises porque também são assíncronos: cada evento é disparado em algum momento no futuro. Porém apesar da semelhança com uma coleção de promises ( como Promise.all( ) ) é importante notar que promises emitem apenas um evento/valor, enquanto que Observables emitem um número arbitrário de eventos ao longo do tempo.

Observables podem ser usados para modelar clicks de botões, onde podem representar todos os clicks que irão ocorrer em todo o ciclo de vida da nossa aplicação.

// Por convenção, o $ (dollar sign) é utilizado no final do nome da variável quando esta é um observable.
let observable$ = cliqueNoBotao(myButton);
Enter fullscreen mode Exit fullscreen mode

Imagine que esse observable irá representar todos os clicks de botões da nossa aplicação. Para podermos acessar os eventos emitidos desses clicks devemos, da mesma forma como .then( ) em promises, utilizar um método chamado subscribe().

O método subscribe() espera receber como argumento uma função que é chamada cada vez que o observable emite um evento. Por exemplo:

let observable$ = cliqueNoBotao(myButton);

// Método subscribe para podermos acessar os eventos de click
// Para cada click, será printada a mensagem abaixo
observable$.subscribe((clickEvent) => console.log("O botão foi clicado"));

// Se quisermos acessar as propriedades dos evento de click, basta utilizarmos o parametro dessa função - nesse caso, clickEvent.
Enter fullscreen mode Exit fullscreen mode

Com o código acima criamos o nosso primeiro observable e acessamos os valores emitidos por ele.

Vale a pena notar, contudo, que observables são preguiçosos - ou lazy. O que isso significa? Se por acaso um observable não tiver nenhum subscriber, nenhum valor será emitido. Ou seja, um observable não curte muito conversar sozinho - se não tiver ninguém ouvindo, ele simplesmente não fala nada!

let observable$ = cliqueNoBotao(myButton);
// Botão foi clicado mas nada é emitido, porque não tem um nenhum subcriber( )
Enter fullscreen mode Exit fullscreen mode

Obs: quando nos referirmos a um subscriber, queremos dizer que nenhum método subscribe( ) foi chamado em um observable.

Vamos colocar em prática esse conhecimento criando um pequeno crônometro, mas antes vamos aprender um pouco sobre diagramas de marble.

Facilitando o entedimento com diagramas de marble

Tratar de observables é tratar de data streams, ou emissão de dados ao longo do tempo - o conceito de streams aqui pode ser resumido em: eventos/dados em andamento ordenados no tempo. Pode ser um conceito inicialmente nebuloso, que é necessário um certo grau de abstração para podermos ter uma visão clara sobre o fluxo de dados que um observable emite. Para facilitar as coisas, vamos aprender um pouco sobre diagramas de marble e podemos tirar proveito deles para criar um modelo mental sobre o fluxo de um observable.

Primeiro temos a timeline (ou linha do tempo), ilustrando a passagem do tempo da emissão dos eventos pelo observables. É representado por uma flecha:

"a timeline de um diagram de marble"
a timeline de um diagram de marble

O segundo elemento são os valores que percorrem a timeline - ou, um fluxo que completou. O | no final da timeline, demonstra que ele terminou:

"a timeline de um diagram de marble"

valores sendo emitidos na timeline

Como podemos ver, a leitura é simples: um circulo foi emitido em determinado momento por um observable. Como normalmente trabalhamos com dados complexos, você pode substituir as formas geometricas pela abstração que quiser - no momento o importante é visualizar esses dados sendo emitidos ao longo do tempo.

Um fluxo que termina com erros - representado pelo X:

"a timeline de um diagram de marble"
um fluxo que terminar com erros

Um fluxo que nunca terminou:

"a timeline de um diagram de marble"
um fluxo infinito

Pronto, isso é tudo que você precisa saber sobre diagramas de marble por agora. Quando avançarmos, vamos ver os mais diferentes fluxos possíveis.

Criando um crônometro

Para fixar os conceitos que aprendemos, iremos construir um pequeno crônometro. O crônomero tem dois botões: Iniciar e Parar. Abaixo desses botões vamos ter a contagem do tempo decorrido, representado segundos e milisegundos. Para todos os projetos que fizermos, vamos utilizar a biblioteca RXJS, a mais mais popular biblioteca do mundo javascript para programação reativa. Usaremos typescript ao invés de vanilla JS nesse artigo e webpack.

O código do projeto vai estar disponível no final do artigo. O setup do projeto pode ser feito seguindo a leitura desse artigo. Se você não quer se preocupar em configurar o ambiente, você pode utilizar o rxjs playground.

Vamos começar criando nosso arquivo HTML com o seguinte conteúdo:

<!-- 

Aqui temos dois botões, um para iniciar o cronometro, outro para parar.

Na tag h2 mostramos o tempo do nosso cronometro. -->
<body>
  <div>
    <div>
      <div>
        <div>
          <h1>Cronometro</h1>
          <button id="botao-iniciar">Iniciar</button>
          <button id="botao-parar">Parar</button>
          <h2 id="tempo">0.0s</h2>
        </div>
      </div>
    </div>
  </div>
  <script src="cronometro.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

A lógica da nossa aplicação é bem simples: teremos 3 observables, um para monitorar o inicio da contagem representado pelo botao Iniciar, outro para parar a contagem representado pelo botão Parar; o terceiro observable será responsável pela contagem em segundos do tempo decorrido, sendo incrementado a cada 1/10 de segundo e atualizar a página com o tempo decorrido.

Primeiramente, no nosso cronometro.js, precisamos importar um observable que será responsável por manter no radar o tempo decorrido em 1/10 de segundos para podermos computar isso no HTML. A biblioteca RXJS conta com várias tipos de operadores. Operadores são funções que podemos utilizar para criar e manipular observables. Para começar, vamos importar o nosso primeiro observable, chamado de Interval(). Em seguido vamos fazer o subscribe para ver o que ele emite.

// aqui importamos os operadores da biblioteca
import { interval } from "rxjs";
// atribuimos a variavel o observable interval;
let decimoDeSegundo$ = interval(100);
// imprimios no console a cada 100 milisegundos o tempo decorrido desde a subscription
decimoDeSegundo$.subscribe((timer) => console.log(timer));
Enter fullscreen mode Exit fullscreen mode

Ainda não é bem o que queremos, aqui estamos recebendo esses valores de forma ainda bruta, precisamos de uma forma de manipular esses valores para que tenhamos a representação de segundos e milisegundos. Para isso, precisamos de um operador que manipule os dados emitidos pelo nosso observable decimoDeSegundo$. Para fazer isso, vamos precisar de um operador chamado map. O map do rxjs funciona quase da mesma forma que o map do javascript, ele recebe uma função e faz uma manipulação dos dados e devolve os novos dados transformados. A diferença aqui é que o map do javascript manipula arrays, e o map do rxjs manipula observables.

Além, disso para podermos aplicar a transformação nos dados do observables, precisa utilizar o operador pipe(). Pipes são usados para encadear operações nos observables de maneira síncrona - isto é, apesar dos dados do observable serem emitidos ao longo do tempo, o map aplica essas transformações instântaneamente. Então, o que queremos fazer é pegar os dados emitidos pelo observable e dividir por 10, assim fica:

let decimoDeSegundo$ = interval(100);

// transformamos cada numero emitido pelo observable e dividimos por 10
decimoDeSegundo$.pipe(map((time) => time / 10));
Enter fullscreen mode Exit fullscreen mode

A próxima coisa que devemos fazer, é mapear dois observables para o click dos botões. Para fazermos isso, primeiro vamos pegar o id dos botões:

import { interval } from "rxjs";
import { map } from "rxjs/operators";

let decimoDeSegundo$ = interval(100);

const botaoIniciar = document.querySelector("#botao-iniciar");

const botaoParar = document.querySelector("#botao-parar");

decimoDeSegundo$.pipe(map((time) => time / 10));
decimoDeSegundo$.subscribe((timer) => console.log(timer));
Enter fullscreen mode Exit fullscreen mode

Agora que são temos a referência dos botões no nosso HTML, devemos criar nossos observables para mapear os eventos emitidos nos botões - para isso, vamos importar um novo operador do RXJS, chamado fromEvent, que transforma os eventos no elemento em observables:

import { interval, fromEvent } from "rxjs";
import { map } from "rxjs/operators";

let decimoDeSegundo$ = interval(100);
const botaoIniciar = document.querySelector("#botao-iniciar");
const botaoParar = document.querySelector("#botao-parar");

//Transformar o "event listener" em um observable.
// fromEvent espera um elemento HTML e um evento a ser disparado
const cliqueIniciar$ = fromEvent(botaoIniciar, "click");
const cliqueParar$ = fromEvent(botaoParar, "click");

decimoDeSegundo$.pipe(map((time) => time / 10));
decimoDeSegundo$.subscribe((timer) => console.log(timer));
Enter fullscreen mode Exit fullscreen mode

Como exercício, faça o subscribe nos cliques, coloque um console log em cada subscribe e veja se está tudo funcionando corretamente. Para cada clique, um log deve ser emitido!

Finalizando o nosso cronômetro

Agora, devemos pegar a tag html para mostrar a contagem de tempo do HTML. Vamos utilizar todos os conceitos discutidos até aqui e juntar tudo no código.

import { interval, fromEvent, pipe } from "rxjs";
import { map, takeUntil } from "rxjs/operators";

let decimoDeSegundo$ = interval(100);
const botaoIniciar = document.querySelector("#botao-iniciar");
const botaoParar = document.querySelector("#botao-parar");
const tempoDisplay = document.querySelector < HTMLElement > "#tempoDisplay";
const cliqueIniciar$ = fromEvent(botaoIniciar, "click");
const cliqueParar$ = fromEvent(botaoParar, "click");

cliqueIniciar$.subscribe(() => {
  decimoDeSegundo$
    .pipe(
      map((timer) => timer / 10),
      takeUntil(cliqueParar$)
    )
    .subscribe((result) => {
      tempoDisplay.innerText = result + "s";
    });
});
Enter fullscreen mode Exit fullscreen mode

Ok, algumas coisas estão acontecendo aqui, vamos quebrar o código e explicar por partes. Acontece o seguinte, o que queremos fazer é quand o usuário clicar no botão iniciar, queremos começar a contagem do nosso tempo. Para isso fazemos um subscribe no botão iniciar, e quando o primeiro evento por disparado, a contagem começa.

Dentro do subscribe do observable cliqueIniciar$, vamos inicializar o outro observable decimoDeSegundo$ que roda o seu construtor, fazer as operações do pipe e atualiza o html com o valor computado. Pode parecer confuso criar um modelo mental sobre quando o pipe efetivamente transforma os dados com as funções map e takeUntil - que vamos explicar logo mais. Então, tenha em mente que as operações do pipe rodam assim que o subscribe acontece, então o que recebemos na nossa variável result já são os valores transformados. Então, você deve estar se perguntando, o que faz takeUntil?

O operador takeUntil é acoplado no fluxo da nossa stream e recebe como paramêtro um outro observable que quando disparado para o fluxo atual de da cadeia de observables. Esse operador faz o que o nome realmente diz, ele emite os dados da stream até que o evento do observable passado como parâmetro é disparado. Ou seja, quando o evento do click parar for disparado, o nosso fluxo do observable decimoSegundo$ é cessado - takeUntil faz automáticamente o unsubscribe dos observables da stream. Mas você deve estar se perguntando, como sabemos que o botão parar foi acionado se não existe nenhum subscriber no observable cliqueParar$? Bom, adicionalmente, takeUntil faz o subscribe e o unsubscribe do observable passado como parâmetro automáticamente.

E então finalizamos o nosso cronômetro. Espero que vocês tenham gostado e qualquer crítica construtiva sobre o artigo pode ser deixado nos comentários.

Links adicionais para leitura:

https://www.learnrxjs.io/

https://rxmarbles.com/

https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

Código:

https://github.com/edward-teixeira/rxjs-cronometro

Top comments (0)