O autor do material, cuja tradução publicamos hoje, diz que aqui ele deseja demonstrar o processo de desenvolvimento de um aplicativo React simples que usa RxJS. Segundo ele, ele não é especialista em RxJS, pois está envolvido no estudo dessa biblioteca e não recusa a ajuda de pessoas com conhecimento. Seu objetivo é chamar a atenção do público para formas alternativas de criação de aplicativos React, para inspirar o leitor a fazer pesquisas independentes. Este material não pode ser chamado de introdução ao RxJS. Uma das muitas maneiras de usar essa biblioteca no desenvolvimento do React será mostrada aqui.

Como tudo começou
Recentemente,
meu cliente me inspirou a aprender sobre o uso do
RxJS para gerenciar o estado dos aplicativos React. Quando auditei o código do aplicativo para esse cliente, ele queria saber minha opinião sobre como ele desenvolveu o aplicativo, já que antes ele usava exclusivamente o estado local do React. O projeto chegou a um ponto em que não era razoável confiar apenas no React. Primeiro, falamos sobre o uso do
Redux ou MobX como um meio melhor de gerenciar o estado do seu aplicativo. Meu cliente criou um protótipo para cada uma dessas tecnologias. Mas ele não parou com essas tecnologias, criando um protótipo de um aplicativo React que usa RxJS. A partir desse momento, nossa conversa se tornou muito mais interessante.
O aplicativo em questão era uma plataforma de negociação de criptomoedas. Ele tinha muitos widgets cujos dados foram atualizados em tempo real. Os desenvolvedores deste aplicativo, entre outros, tiveram que resolver as seguintes tarefas difíceis:
- Gerenciar várias solicitações de dados (assíncronas).
- Atualização em tempo real de um grande número de widgets no painel de controle.
- A solução para o problema de widgets e conectividade de dados, pois alguns widgets precisavam de dados não apenas de algumas fontes especiais, mas também de outros widgets.
Como resultado, as principais dificuldades que os desenvolvedores enfrentaram não estavam relacionadas à própria biblioteca do React e, além disso, eu poderia ajudá-los nessa área. O principal problema era fazer com que os mecanismos internos do sistema funcionassem corretamente, aqueles que vinculavam dados de criptomoeda e elementos de interface criados pelas ferramentas React. Foi nessa área que os recursos do RxJS foram úteis, e o protótipo que eles me mostraram parecia muito promissor.
Usando RxJS em React
Suponha que tenhamos um aplicativo que, depois de executar algumas ações locais, faça solicitações para uma
API de terceiros . Permite pesquisar por artigo. Antes de executar a solicitação, precisamos obter o texto usado para formar essa solicitação. Em particular, nós, usando este texto, formamos um URL para acessar a API. Aqui está o código do componente React que implementa essa funcionalidade
import React from 'react'; const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default App;
Este componente não possui um sistema de gerenciamento de estado. O estado da propriedade da
query
não é armazenado em nenhum lugar, a função
onChangeQuery
também não atualiza o estado. Na abordagem convencional, esse componente é equipado com um sistema de gerenciamento de estado local. É assim:
class App extends React.Component { constructor(props) { super(props); this.state = { query: '', }; } onChangeQuery = query => { this.setState({ query }); }; render() { return ( <div> <h1>React with RxJS</h1> <input type="text" value={this.state.query} onChange={event => this.onChangeQuery(event.target.value) } /> <p>{`http:
No entanto, não é essa a abordagem sobre a qual falaremos aqui. Em vez disso, queremos estabelecer um sistema de gerenciamento de estado de aplicativo usando RxJS. Vamos ver como fazer isso usando o componente de ordem superior (HOC).
Se desejar, você pode implementar lógica semelhante no seu componente
App
, mas provavelmente, em algum momento do seu trabalho no aplicativo, decida projetar esse componente na forma de um
HOC adequado para reutilização.
Componentes React e RxJS de primeira ordem
Vamos descobrir como gerenciar o estado dos aplicativos React usando o RxJS, usando um componente de ordem superior para esse fim. Em vez disso, pode-se implementar
o modelo de adereços de renderização. Como resultado, se você não quiser criar um componente de ordem superior para esse fim, use os componentes de ordem superior observáveis.
mapPropsStream()
com
mapPropsStream()
e
componentFromStream()
. Neste guia, no entanto, faremos tudo sozinhos.
import React from 'react'; const withObservableStream = (...) => Component => { return class extends React.Component { componentDidMount() {} componentWillUnmount() {} render() { return ( <Component {...this.props} {...this.state} /> ); } }; }; const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default withObservableStream(...)(App);
Enquanto o componente de ordem superior RxJS não executa nenhuma ação. Ele transfere apenas seu próprio estado e propriedades para o componente de entrada, que está planejado para ser expandido com sua ajuda. Como você pode ver, no final, um componente de ordem superior estará envolvido no gerenciamento do estado React. No entanto, esse estado será obtido a partir do fluxo observado. Antes de começarmos a implementar o HOC e usá-lo com o componente
App
, precisamos instalar o RxJS:
npm install rxjs
Agora vamos começar a usar um componente de ordem superior e implementar sua lógica:
import React from 'react'; import { BehaviorSubject } from 'rxjs'; ... const App = ({ query, onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); const query$ = new BehaviorSubject({ query: 'react' }); export default withObservableStream( query$, { onChangeQuery: value => query$.next({ query: value }), } )(App);
O componente do
App
si não muda. Acabamos de passar dois argumentos para um componente de ordem superior. Nós os descrevemos:
- Objeto observado. O argumento de
query
é um objeto observável que possui um valor inicial, mas, além disso, retorna, ao longo do tempo, novos valores (já que esse é um BehaviorSubject
). Qualquer pessoa pode se inscrever neste objeto observável. Aqui está o que a documentação do RxJS diz sobre objetos do tipo BehaviorSubject
: “Uma das opções para objetos Subject
é um BehaviorSubject
que usa o conceito de“ valor atual ”. Ele armazena o último valor passado aos seus assinantes e, quando um novo observador se inscreve, recebe imediatamente esse "valor atual" do BehaviorSubject
. Esses objetos são adequados para a apresentação de dados, cujas novas partes aparecem com o tempo ". - O sistema para emitir novos valores para o objeto observado (gatilho). A função
onChangeQuery()
, passada pelo HOC para o componente App
, é uma função regular que transmite o próximo valor ao objeto observado. Essa função é transferida no objeto, pois pode ser necessário transferir para um componente de ordem superior várias funções que executam determinadas ações com os objetos observados.
Após criar o objeto observado e assinar o mesmo, o fluxo da solicitação deve funcionar. No entanto, até agora, o próprio componente de ordem superior parece uma caixa preta para nós. Nós percebemos isso:
const withObservableStream = (observable, triggers) => Component => { return class extends React.Component { componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; };
Um componente de ordem superior recebe um objeto observável e um objeto com gatilhos (talvez esse objeto que contenha funções possa ser chamado de termo mais bem-sucedido do léxico RxJS), representado na assinatura da função.
Os gatilhos são passados apenas pelo HOC para o componente de entrada. É por isso que o componente
App
recebe diretamente a função
onChangeQuery()
, que trabalha diretamente com o objeto observado, passando novos valores para ele.
O objeto observado usa o método do ciclo de vida
componentDidMount()
para assinar e o método
componentDidMount()
para cancelar a assinatura. É necessário cancelar a inscrição para evitar
vazamentos de memória . Na assinatura do objeto observado, a função envia apenas todos os dados recebidos do fluxo para o armazenamento de estado React local usando o comando
this.setState()
.
Vamos fazer uma pequena alteração no componente
App
, que eliminará o problema que ocorre se o componente de ordem superior não definir o valor inicial da propriedade da
query
. Se isso não for feito, no início do trabalho, a propriedade da
query
ficará
undefined
. Graças a essa alteração, essa propriedade obtém o valor padrão.
const App = ({ query = '', onChangeQuery }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> );
Outra maneira de lidar com esse problema é definir o estado inicial da
query
em um componente de ordem superior:
const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component { constructor(props) { super(props); this.state = { ...initialState, }; } componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; }; const App = ({ query, onChangeQuery }) => ( ... ); export default withObservableStream( query$, { onChangeQuery: value => query$.next({ query: value }), }, { query: '', } )(App);
Se você tentar este aplicativo agora, a caixa de entrada deve funcionar conforme o esperado. O componente
App
recebe do HOC, na forma de propriedades, apenas o estado da
query
e a função
onChangeQuery
para alterar o estado.
A recuperação e a alteração do estado ocorrem por meio de objetos observáveis do RxJS, apesar do fato de que um armazenamento interno do estado React é usado dentro do componente de ordem superior. Não consegui encontrar uma solução óbvia para o problema de transmitir dados de uma assinatura de objetos observados diretamente para as propriedades do componente estendido (
App
). É por isso que eu tive que usar o estado local React na forma de uma camada intermediária, o que, além disso, é conveniente em termos do que causa a nova renderização. Se você conhece outra maneira de atingir os mesmos objetivos, pode compartilhá-la nos comentários.
Combinando objetos observados no React
Vamos criar um segundo fluxo de valores, que, como a propriedade
query
, pode ser usado no componente
App
. Mais tarde, usaremos os dois valores, trabalhando com eles usando outro objeto observável.
const SUBJECT = { POPULARITY: 'search', DATE: 'search_by_date', }; const App = ({ query = '', subject, onChangeQuery, onSelectSubject, }) => ( <div> <h1>React with RxJS</h1> <input type="text" value={query} onChange={event => onChangeQuery(event.target.value)} /> <div> {Object.values(SUBJECT).map(value => ( <button key={value} onClick={() => onSelectSubject(value)} type="button" > {value} </button> ))} </div> <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> </div> );
Como você pode ver, o parâmetro
subject
pode ser usado para refinar a solicitação ao construir a URL usada para acessar a API. Ou seja, os materiais podem ser pesquisados com base em sua popularidade ou na data de publicação. Em seguida, crie outro objeto observável que possa ser usado para alterar o parâmetro
subject
. Este observável pode ser usado para associar um componente de
App
a um componente de ordem superior. Caso contrário, as propriedades passadas para o componente
App
não funcionarão.
import React from 'react'; import { BehaviorSubject, combineLatest } from 'rxjs/index'; ... const query$ = new BehaviorSubject({ query: 'react' }); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({ subject, query, })), { onChangeQuery: value => query$.next({ query: value }), onSelectSubject: subject => subject$.next(subject), }, )(App);
O gatilho
onSelectSubject()
não é novo. Através de um botão, ele pode ser usado para alternar entre dois estados de
subject
. Mas o objeto observado transferido para um componente de ordem superior é algo novo. Ele usa a função
combineLatest()
do RxJS para combinar os últimos valores retornados de dois (ou mais) fluxos observados. Depois de assinar o objeto observado, se algum dos valores (
query
ou
subject
) for alterado, o assinante receberá os dois valores.
Um complemento para o mecanismo implementado pela função
combineLatest()
é seu último argumento. Aqui você pode definir a ordem de retorno dos valores gerados pelos objetos observados. No nosso caso, precisamos que eles sejam representados como um objeto. Isso permitirá, como antes, destruí-los em um componente de ordem superior e gravá-los no estado React local. Como já temos a estrutura necessária, podemos omitir a etapa de agrupar o objeto do objeto de
query
observado.
... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({ subject, query, })), { onChangeQuery: value => query$.next(value), onSelectSubject: subject => subject$.next(subject), }, )(App);
O objeto de origem
{ query: '', subject: 'search' }
, bem como todos os outros objetos retornados pelo fluxo combinado de objetos observados, são adequados para destruí-los em um componente de ordem superior e para gravar os valores correspondentes no estado React local. Depois de atualizar o estado, como antes, a renderização é executada. Ao iniciar o aplicativo atualizado, você poderá alterar os dois valores usando o campo e o botão de entrada. Os valores alterados afetam o URL usado para acessar a API. Mesmo se apenas um desses valores for alterado, o outro valor manterá seu último estado, pois a função
combineLatest()
sempre combina os valores mais recentes emitidos a partir dos fluxos observados.
Axios e RxJS em React
Agora em nosso sistema, a URL para acessar a API é construída com base em dois valores de um objeto observável combinado, que inclui outros dois objetos observáveis. Nesta seção, usaremos o URL para carregar dados da API. É possível que você possa usar o
sistema React data loading , mas ao usar objetos observáveis RxJS, é necessário adicionar outro fluxo observável ao nosso aplicativo.
Antes de começarmos a trabalhar no próximo objeto observado, instale
axios . Essa é a biblioteca que usaremos para carregar dados de fluxos no programa.
npm install axios
Agora imagine que temos uma variedade de artigos que o componente
App
deve gerar. Aqui, como o valor do parâmetro padrão correspondente, usamos uma matriz vazia, fazendo o mesmo que fizemos com outros parâmetros.
... const App = ({ query = '', subject, stories = [], onChangeQuery, onSelectSubject, }) => ( <div> ... <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> <ul> {stories.map(story => ( <li key={story.objectID}> <a href={story.url || story.story_url}> {story.title || story.story_title} </a> </li> ))} </ul> </div> );
Para cada artigo da lista, um valor de fallback é usado devido ao fato de que a API que estamos acessando não é uniforme. Agora - o mais interessante - a implementação de um novo objeto observável responsável pelo carregamento de dados em um aplicativo React que os visualiza.
import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { flatMap, map } from 'rxjs/operators'; ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); const fetch$ = combineLatest(subject$, query$).pipe( flatMap(([subject, query]) => axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
Um novo objeto observável é, novamente, uma combinação de observáveis de
subject
e
query
, pois, para construir a URL com a qual acessaremos a API para carregar dados, precisamos dos dois valores. No método
pipe()
do objeto observado, podemos usar os chamados "operadores RxJS" para executar determinadas ações com os valores. Nesse caso, mapeamos dois valores que são colocados na consulta, que os axios usam para obter o resultado. Aqui usamos o operador
flatMap()
e não
map()
para acessar o resultado da promessa resolvida com sucesso, e não a promessa retornada. Como resultado, após inscrever-se nesse novo objeto observado, toda vez que um novo
subject
ou valor de
query
é inserido no sistema a partir de outros objetos observados, uma nova
query
é executada e o resultado está na função de inscrição.
Agora, novamente, podemos fornecer um novo observável para um componente de ordem superior. Temos o último argumento para a função
combineLatest()
nossa disposição, isso torna possível mapeá-lo diretamente para uma propriedade chamada
stories
. No final, isso representa como esses dados já estão sendo usados no componente
App
.
export default withObservableStream( combineLatest( subject$, query$, fetch$, (subject, query, stories) => ({ subject, query, stories, }), ), { onChangeQuery: value => query$.next(value), onSelectSubject: subject => subject$.next(subject), }, )(App)
Não há gatilho aqui, uma vez que o objeto observado é indiretamente ativado por outros dois fluxos observados. Cada vez que o valor no campo de entrada (
query
) é alterado ou o botão (
subject
) é clicado, isso afeta o objeto de
fetch
observado, que contém os valores mais recentes de ambos os fluxos.
No entanto, talvez não precisemos que sempre que alteramos o valor no campo de entrada, isso afetará o objeto de
fetch
observado. Além disso, não queremos que a
fetch
seja afetada se o valor for uma sequência vazia. É por isso que podemos estender o objeto de
query
observável usando a instrução
debounce
, que elimina alterações de consulta muito frequentes. Ou seja, graças a esse mecanismo, um novo evento é aceito somente após um tempo predeterminado após o evento anterior. Além disso, usamos o operador de
filter
aqui, que filtra os eventos de fluxo se a sequência de
query
estiver vazia.
import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest, timer } from 'rxjs'; import { flatMap, map, debounce, filter } from 'rxjs/operators'; ... const queryForFetch$ = query$.pipe( debounce(() => timer(1000)), filter(query => query !== ''), ); const fetch$ = combineLatest(subject$, queryForFetch$).pipe( flatMap(([subject, query]) => axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
A instrução
debounce
faz o trabalho de inserir dados no campo. No entanto, quando um botão clica no valor do
subject
, a solicitação deve ser executada imediatamente.
Agora, os valores iniciais para
query
e
subject
, que vemos quando o componente
App
é exibido pela primeira vez, não são os mesmos que os obtidos a partir dos valores iniciais dos objetos observados:
const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY);
subject
gravado no
subject
, uma sequência vazia está na
query
. Isso se deve ao fato de que foram esses valores que fornecemos como parâmetros padrão para destruir a função do componente
App
na assinatura. O motivo disso é que precisamos aguardar a solicitação inicial executada pelo objeto de
fetch
observável. Como não sei exatamente como obter imediatamente valores da
query
observada e dos objetos de
subject
em um componente de ordem superior para gravá-los em um estado local, decidi configurar o estado inicial do componente de ordem superior novamente.
const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component { constructor(props) { super(props); this.state = { ...initialState, }; } componentDidMount() { this.subscription = observable.subscribe(newState => this.setState({ ...newState }), ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { return ( <Component {...this.props} {...this.state} {...triggers} /> ); } }; };
Agora o estado inicial pode ser fornecido a um componente de ordem superior como um terceiro argumento. No futuro, podemos remover as configurações padrão do componente
App
.
... const App = ({ query, subject, stories, onChangeQuery, onSelectSubject, }) => ( ... ); export default withObservableStream( combineLatest( subject$, query$, fetch$, (subject, query, stories) => ({ subject, query, stories, }), ), { onSelectSubject: subject => subject$.next(subject), onChangeQuery: value => query$.next(value), }, { query: 'react', subject: SUBJECT.POPULARITY, stories: [], }, )(App);
O que me preocupa agora é que o estado inicial também é definido na declaração dos objetos observados,
query$
e
subject$
. Essa abordagem é propensa a erros, pois a inicialização dos objetos observados e o estado inicial do componente de ordem superior compartilham os mesmos valores. Eu teria gostado mais se, em vez disso, os valores iniciais fossem extraídos dos objetos observados em um componente de ordem superior para definir o estado inicial. Talvez um dos leitores deste material possa compartilhar conselhos nos comentários sobre como fazer isso.
O código para o projeto de software que fizemos aqui pode ser encontrado
aqui .
Sumário
O objetivo principal deste artigo é demonstrar uma abordagem alternativa para o desenvolvimento de aplicativos React usando o RxJS. Esperamos que ele tenha lhe dado um pensamento. Às vezes, Redux e MobX não são necessários, mas talvez nessas situações o RxJS seja exatamente o que é adequado para um projeto específico.
Caros leitores! Você usa o RxJS ao desenvolver aplicativos React?
