Como fazer pesquisas de usuários no GitHub usando React + RxJS 6 + Recompose

Uma imagem para atrair a atenção


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), // <---      map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

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), // <--   map(user => ( <h3>{user}</h3> )) ); 

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 // <--- } from 'rxjs/operators'; 

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 :)

Source: https://habr.com/ru/post/pt419559/


All Articles