
Este artigo é para pessoas com experiência com React e RxJS. Estou apenas compartilhando modelos que achei úteis para criar uma interface do usuário.
Aqui está o que fazemos:

Sem classes, trabalhando com um ciclo de vida ou setState
.
Preparação
Tudo que você precisa está no meu repositório no GitHub.
git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install
No ramo master
, há um projeto concluído. Alterne para o ramo start
, se você quiser ir passo a passo.
git checkout start
E execute o projeto.
npm start
O aplicativo deve iniciar no localhost:3000
e aqui está nossa interface do usuário inicial.

Inicie seu editor favorito e abra o src/index.js
.

Recompor
Se você não está familiarizado com o Recompose , esta é uma biblioteca maravilhosa que permite criar componentes do React em um estilo funcional. Ele contém um grande conjunto de funções. Aqui estão os meus favoritos .
É como Lodash / Ramda, apenas Reage.
Também estou muito feliz por apoiar o padrão Observer. Citando a documentação :
Acontece que a maior parte da API do React Component pode ser expressa em termos do padrão Observer.
Hoje vamos praticar esse conceito!
Componente em linha
Até agora, temos o App
- o componente React mais comum. Usando a função componentFromStream
da biblioteca Recompose, podemos obtê-la através de um objeto observável.
A função componentFromStream
inicia a renderização em cada novo valor a partir do nosso observável. Se ainda não houver valores, ele será null
.
Configuração
Os fluxos no Recompose seguem o documento da proposta observável do ECMAScript . Ele descreve como os objetos observáveis devem funcionar quando implementados em navegadores modernos.
Enquanto isso, usaremos bibliotecas como RxJS, xstream, most, Flyd, etc.
Recompose não sabe qual biblioteca usamos, portanto fornece a função setObservableConfig
. Com ele, você pode converter tudo o que precisamos em um ES Observable.
Crie um novo arquivo na pasta src
e observableConfig.js
-o observableConfig.js
.
Para conectar o RxJS 6 ao Recompose, escreva o seguinte código:
import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from });
Importe este arquivo para index.js
:
import './observableConfig';
Isso é tudo!
Recompor + RxJS
Adicione a importação componentFromStream
ao index.js
:
import { componentFromStream } from 'recompose';
Vamos começar a substituir o componente App
:
const App = componentFromStream(prop$ => { ... });
Observe que componentFromStream
usa como argumento uma função com o parâmetro prop$
, que é uma versão observável de props
. A idéia é usar o mapa para transformar props
regulares em componentes do React.
Se você usou o RxJS, deve estar familiarizado com o operador de mapa .
Mapa
Como o nome sugere, o mapa transforma Observable(something)
em Observable(somethingElse)
. No nosso caso - Observable(props)
em Observable(component)
.
Importe o operador de map
:
import { map } from 'rxjs/operators';
Complemente nosso componente App
:
const App = componentFromStream(prop$ => { return prop$.pipe( map(() => ( <div> <input placeholder="GitHub username" /> </div> )) ) });
Com o RxJS 5, usamos pipe
vez de uma cadeia de instruções.
Salve o arquivo e verifique o resultado. Nada mudou!

Adicionar um manipulador de eventos
Agora vamos tornar nosso campo de entrada um pouco reativo.
Adicione a importação createEventHandler
:
import { componentFromStream, createEventHandler } from 'recompose';
Vamos usá-lo assim:
const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) });
O objeto criado por createEventHandler
possui dois campos interessantes: handler
e stream
.
Sob o capô, handler
é um emissor de eventos que transfere valores para o stream
. E stream
por sua vez, é um objeto observável que transmite valores aos assinantes.
Vamos vincular stream
e prop$
para obter o valor atual do campo de entrada.
No nosso caso, uma boa opção seria usar a função combineLatest
.
Problema com ovo e galinha
Para usar o combineLatest
, stream
e prop$
devem produzir valores. Mas o stream
não lançará nada até que algum valor libere prop$
e vice-versa.
Você pode corrigir isso definindo stream
valor inicial.
Importe a instrução startWith do RxJS:
import { map, startWith } from 'rxjs/operators';
Crie uma nova variável para obter o valor do stream
atualizado:
// App component const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value) startWith('') );
Sabemos que o stream
lançará eventos quando o campo de entrada mudar, então vamos traduzi-los imediatamente em texto.
E como o valor padrão do campo de entrada é uma sequência vazia, inicialize o value$
object com o value$
''
Tricotar juntos
Agora estamos prontos para conectar os dois fluxos. Importe combineLatest
como um método de criação de objetos Observable, não como um operador .
import { combineLatest } from 'rxjs';
Você também pode importar uma instrução de tap
para examinar os valores de entrada.
import { map, startWith, tap } from 'rxjs/operators';
Use-o assim:
const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value), startWith('') ); return combineLatest(prop$, value$).pipe( tap(console.warn),
Agora, se você começar a digitar algo em nosso campo de entrada, os valores [props, value]
aparecerão no console.

Componente do usuário
Este componente será responsável por exibir o usuário cujo nome iremos transferir para ele. Ele receberá value
do componente App
e o converte em uma solicitação AJAX.
Jsx / css
Tudo isso é baseado no maravilhoso projeto GitHub Cards . A maioria dos códigos, especialmente os estilos, é copiada ou adaptada.
Crie a pasta src/User
. Crie um arquivo User.css
e copie esse código para ele.
E copie esse código no arquivo src/User/Component.js
.
Esse componente simplesmente preenche o modelo com dados de uma chamada para a API do GitHub.
Container
Agora, este componente é "burro" e não estamos na estrada, vamos fazer um componente "inteligente".
Aqui está src/User/index.js
import React from 'react'; import { componentFromStream } from 'recompose'; import { debounceTime, filter, map, pluck } from 'rxjs/operators'; import Component from './Component'; import './User.css'; const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(user => ( <h3>{user}</h3> )) ); return getUser$; }); export default User;
Definimos User
como componentFromStream
, que retorna um objeto prop$
Observable prop$
que converte as propriedades recebidas em <h3>
.
debounceTime
Nosso User
receberá novos valores cada vez que uma tecla é pressionada no teclado, mas não precisamos desse comportamento.
Quando o usuário começa a digitar, debounceTime(1000)
pula todos os eventos que duram menos de um segundo.
arrancar
Esperamos que o objeto do user
seja passado como props.user
. O operador de remoção pega o campo especificado do objeto e retorna seu valor.
filtrar
Aqui, garantimos que o user
aprovado e não seja uma string vazia.
mapa
Nós <h3>
tag <h3>
do user
.
Conectar
src/index.js
para src/index.js
e importe o componente User
:
import User from './User';
Passamos o valor do value
como o parâmetro do user
:
return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( <div> <input onChange={handler} placeholder="GitHub username" /> <User user={value} /> </div> )) );
Agora nosso valor é exibido com um atraso de um segundo.

Nada mal, agora precisamos obter informações sobre o usuário.
Pedido de dados
O GitHub fornece uma API para obter informações do usuário: https://api.github.com/users/${user} . Podemos escrever facilmente uma função auxiliar:
const formatUrl = user => `https://api.github.com/users/${user}`;
E agora podemos adicionar o map(formatUrl)
após o filter
:
const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl),
E agora, em vez do nome de usuário, a tela exibe o URL.
Precisamos fazer um pedido! switchMap
e ajax
vêm em switchMap
.
switchMap
Este operador é ideal para alternar entre vários observáveis.
Digamos que o usuário digitou o nome e faremos uma solicitação dentro do switchMap
.
O que acontece se o usuário digitar algo antes que a resposta da API chegue? Devemos nos preocupar com solicitações anteriores?
Não.
A switchMap
cancelará a solicitação antiga e passará para a nova.
ajax
O RxJS fornece sua própria implementação de ajax
que funciona muito bem com o switchMap
!
Experimente
Importamos os dois operadores. Meu código fica assim:
import { ajax } from 'rxjs/ajax'; import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators';
E use-os assim:
const User = componentFromStream(prop$ => { const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), switchMap(url => ajax(url).pipe( pluck('response'), map(Component) ) ) ); return getUser$; });
A switchMap
alterna do nosso campo de entrada para uma solicitação AJAX. Quando uma resposta chega, ela passa para o nosso componente burro.
E aqui está o resultado!

Tratamento de erros
Tente digitar um nome de usuário inexistente.

Nosso aplicativo está quebrado.
catchError
Com o operador catchError
podemos exibir uma resposta sensata em vez de catchError
silenciosamente.
Nós importamos:
import { catchError, debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators';
E insira-o no final de nossa solicitação AJAX:
switchMap(url => ajax(url).pipe( pluck('response'), map(Component), catchError(({ response }) => alert(response.message)) ) )

Já não é ruim, mas é claro que você pode fazer melhor.
Erro de componente
Crie o src/Error/index.js
com o conteúdo:
import React from 'react'; const Error = ({ response, status }) => ( <div className="error"> <h2>Oops!</h2> <b> {status}: {response.message} </b> <p>Please try searching again.</p> </div> ); export default Error;
Ele exibirá bem a response
e o status
nossa solicitação AJAX.
Nós o importamos para User/index.js
e, ao mesmo tempo, o operador of do RxJS:
import Error from '../Error'; import { of } from 'rxjs';
Lembre-se de que a função passada para componentFromStream
deve retornar observável. Podemos conseguir isso usando o operador of:
ajax(url).pipe( pluck('response'), map(Component), catchError(error => of(<Error {...error} />)) )
Agora nossa interface do usuário parece muito melhor:

Indicador de carregamento
É hora de introduzir o gerenciamento de estado. De que outra forma você pode implementar o indicador de carregamento?
E se o lugar setState
usaremos BehaviorSubject
?
A documentação de recomposição sugere o seguinte:
Em vez de setState (), combine vários threads
Ok, você precisa de duas novas importações:
import { BehaviorSubject, merge, of } from 'rxjs';
O BehaviorSubject
conterá o status do download e a merge
o associará ao componente.
componentFromStream
internoFromStream:
const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ...
Um BehaviorSubject
inicializado com um valor inicial ou "estado". Como não estamos fazendo nada até que o usuário comece a digitar o texto, inicialize-o como false
.
Mudaremos o estado de loading$
usando o operador tap
:
import { catchError, debounceTime, filter, map, pluck, switchMap, tap
Vamos usá-lo assim:
const loading$ = new BehaviorSubject(false); const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), tap(() => loading$.next(true)), // <--- switchMap(url => ajax(url).pipe( pluck('response'), map(Component), tap(() => loading$.next(false)), // <--- catchError(error => of(<Error {...error} />)) ) ) );
Logo antes da solicitação switchMap
e AJAX, passamos true
para o valor loading$
e false
após a resposta bem-sucedida.
E agora apenas conectamos loading$
e getUser$
.
return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) );
Antes de analisarmos o trabalho, podemos importar a declaração de delay
para que as transições não sejam muito rápidas.
import { catchError, debounceTime, delay, filter, map, pluck, switchMap, tap } from 'rxjs/operators';
Adicionar delay
antes do map(Component)
:
ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) )
Resultado?

All :)