Cómo hacer búsquedas de usuarios en GitHub usando React + RxJS 6 + Recompose

Una imagen para llamar la atención.


Este artículo es para personas con experiencia con React y RxJS. Solo estoy compartiendo plantillas que encontré útiles para crear una interfaz de usuario de este tipo.


Esto es lo que hacemos:



Sin clases, trabajando con un ciclo de vida o setState .


Preparación


Todo lo que necesitas está en mi repositorio en GitHub.


 git clone https://github.com/yazeedb/recompose-github-ui cd recompose-github-ui yarn install 

En la rama master hay un proyecto terminado. Cambie a la rama de start si desea ir paso a paso.


 git checkout start 

Y ejecuta el proyecto.


 npm start 

La aplicación debe comenzar en localhost:3000 y aquí está nuestra IU inicial.



Inicie su editor favorito y abra el src/index.js .



Recomponer


Si no está familiarizado con Recompose , esta es una biblioteca maravillosa que le permite crear componentes React en un estilo funcional. Contiene un gran conjunto de funciones. Aquí están mis favoritos .


Es como Lodash / Ramda, solo reacciona.


También estoy muy contento de que sea compatible con el patrón Observador. Citando la documentación :


Resulta que la mayor parte de la API React Component puede expresarse en términos del patrón de Observador.

¡Hoy practicaremos este concepto!


Componente en línea


Hasta ahora, tenemos la App , el componente React más común. Usando la función componentFromStream de la biblioteca Recompose podemos obtenerla a través de un objeto observable.


La función componentFromStream inicia el renderizado en cada nuevo valor desde nuestro observable. Si todavía no hay valores, se null .


Configuracion


Las secuencias en Recompose siguen el documento de propuesta observable ECMAScript . Describe cómo deberían funcionar los objetos Observables cuando se implementan en navegadores modernos.


Mientras tanto, utilizaremos bibliotecas como RxJS, xstream, most, Flyd, etc.


Recompose no sabe qué biblioteca utilizamos, por lo que proporciona la función setObservableConfig . Con él, puede convertir todo lo que necesitamos en un ES Observable.


Cree un nuevo archivo en la carpeta src y asígnele el nombre observableConfig.js .


Para conectar RxJS 6 a Recompose, escriba el siguiente código:


 import { from } from 'rxjs'; import { setObservableConfig } from 'recompose'; setObservableConfig({ fromESObservable: from }); 

Importe este archivo en index.js :


 import './observableConfig'; 

Eso es todo!


Recomponer + RxJS


Agregue import de componentFromStream a index.js :


 import { componentFromStream } from 'recompose'; 

Comencemos por anular el componente de la App :


 const App = componentFromStream(prop$ => { ... }); 

Tenga en cuenta que componentFromStream toma como argumento una función con el parámetro prop$ , que es una versión observable de props . La idea es usar el mapa para convertir los props regulares en componentes React.


Si utilizó RxJS, debe estar familiarizado con el operador del mapa .


Mapa


Como su nombre lo indica, el mapa convierte Observable(something) en Observable(somethingElse) . En nuestro caso - Observable(props) en Observable(component) .


Importar el operador del map :


 import { map } from 'rxjs/operators'; 

Complemente nuestro componente de App :


 const App = componentFromStream(prop$ => { return prop$.pipe( map(() => ( <div> <input placeholder="GitHub username" /> </div> )) ) }); 

Con RxJS 5, usamos pipe lugar de una cadena de declaraciones.


Guarde el archivo y verifique el resultado. Nada ha cambiado!



Agregar un controlador de eventos


Ahora haremos que nuestro campo de entrada sea un poco reactivo.


Agregue la importación createEventHandler :


 import { componentFromStream, createEventHandler } from 'recompose'; 

Lo usaremos así:


 const App = componentFromStream(prop$ => { const { handler, stream } = createEventHandler(); return prop$.pipe( map(() => ( <div> <input onChange={handler} placeholder="GitHub username" /> </div> )) ) }); 

El objeto creado por createEventHandler tiene dos campos interesantes: handler y stream .


Debajo del capó, el handler es un emisor de eventos que transfiere valores a la stream . Y stream a su vez, es un objeto observable que pasa valores a los suscriptores.


Vincularemos stream y prop$ para obtener el valor actual del campo de entrada.


En nuestro caso, una buena opción sería utilizar la función combineLatest .


Problema de huevo y pollo


Para usar combineLatest , tanto stream como prop$ deben producir valores. Pero stream no lanzará nada hasta que algún valor libere prop$ y viceversa.


Puede solucionar esto configurando la stream valor inicial.


Importe la instrucción startWith de RxJS:


 import { map, startWith } from 'rxjs/operators'; 

Cree una nueva variable para obtener el valor de la stream actualizada:


 // App component const { handler, stream } = createEventHandler(); const value$ = stream.pipe( map(e => e.target.value) startWith('') ); 

Sabemos que la stream generará eventos cuando cambie el campo de entrada, así que traduzcamos inmediatamente a texto.


Y dado que el valor predeterminado para el campo de entrada es una cadena vacía, inicialice el value$ objeto con el value$ ''


Tejer juntos


Ahora estamos listos para conectar ambas transmisiones. Importe combineLatest como un método para crear objetos Observables, no como un operador .


 import { combineLatest } from 'rxjs'; 

También puede importar una declaración de tap para examinar los valores de entrada.


 import { map, startWith, tap } from 'rxjs/operators'; 

Úselo así:


 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> )) ) }); 

Ahora, si comienza a escribir algo en nuestro campo de entrada, los valores [props, value] aparecerán en la consola.



Componente de usuario


Este componente será responsable de mostrar al usuario cuyo nombre le transferiremos. Recibirá value del componente de la App y lo traducirá en una solicitud AJAX.


Jsx / css


Todo esto se basa en el maravilloso proyecto GitHub Cards . La mayoría del código, especialmente los estilos, se copia o adapta.


Cree la carpeta src/User . Cree un archivo User.css y copie este código en él.


Y copie este código en el archivo src/User/Component.js .


Este componente simplemente llena la plantilla con datos de una llamada a la API de GitHub.


Contenedor


Ahora este componente es "tonto" y no estamos en el camino, hagamos un componente "inteligente".


Aquí 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 devuelve un objeto Observable prop$ que convierte las propiedades entrantes en <h3> .


debounceTime


Nuestro User recibirá nuevos valores cada vez que se presione una tecla en el teclado, pero no necesitamos este comportamiento.


Cuando el usuario comienza a escribir, debounceTime(1000) omitirá todos los eventos que duran menos de un segundo.


arrancar


Esperamos que el objeto de user se pase como props.user . El operador de extracción toma el campo especificado del objeto y devuelve su valor.


filtro


Aquí nos aseguramos de que el user pase y no sea una cadena vacía.


mapa


Hacemos la etiqueta <h3> del user .


Conectar


Regrese a src/index.js e importe el componente User :


 import User from './User'; 

Pasamos el valor del value como parámetro de user :


  return combineLatest(prop$, value$).pipe( tap(console.warn), map(([props, value]) => ( <div> <input onChange={handler} placeholder="GitHub username" /> <User user={value} /> </div> )) ); 

Ahora nuestro valor se muestra con un retraso de un segundo.



No está mal, ahora necesitamos obtener información sobre el usuario.


Solicitud de datos


GitHub proporciona una API para obtener información del usuario: https://api.github.com/users/${user} . Podemos escribir fácilmente una función auxiliar:


 const formatUrl = user => `https://api.github.com/users/${user}`; 

Y ahora podemos agregar el map(formatUrl) después del filter :


 const getUser$ = prop$.pipe( debounceTime(1000), pluck('user'), filter(user => user && user.length), map(formatUrl), // <--   map(user => ( <h3>{user}</h3> )) ); 

Y ahora, en lugar del nombre de usuario, la pantalla muestra la URL.


¡Necesitamos hacer una solicitud! switchMap y ajax vienen al switchMap .


switchMap


Este operador es ideal para cambiar entre múltiples observables.


Digamos que el usuario escribió el nombre, y haremos una solicitud dentro de switchMap .


¿Qué sucede si el usuario ingresa algo antes de que llegue la respuesta de la API? ¿Deberíamos estar preocupados por las solicitudes anteriores?


No


La switchMap cancelará la solicitud anterior y cambiará a la nueva.


ajax


¡RxJS proporciona su propia implementación ajax que funciona muy bien con switchMap !


Prueba


Importamos ambos operadores. Mi código se ve así:


 import { ajax } from 'rxjs/ajax'; import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

Y úsalos así:


 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$; }); 

La switchMap cambia de nuestro campo de entrada a una solicitud AJAX. Cuando llega una respuesta, la pasa a nuestro componente tonto.


¡Y aquí está el resultado!



Manejo de errores


Intenta ingresar un nombre de usuario inexistente.



Nuestra aplicación está rota.


catchError


Con el operador catchError podemos mostrar una respuesta catchError lugar de catchError silencio.


Importamos:


 import { catchError, debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators'; 

E insértelo al final de nuestra solicitud AJAX:


 switchMap(url => ajax(url).pipe( pluck('response'), map(Component), catchError(({ response }) => alert(response.message)) ) ) 


Ya no está mal, pero por supuesto que puedes hacerlo mejor.


Error de componente


Cree el src/Error/index.js con el contenido:


 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; 

Mostrará muy bien la response y el status nuestra solicitud AJAX.


Lo importamos a User/index.js y, al mismo tiempo, al operador de RxJS:


 import Error from '../Error'; import { of } from 'rxjs'; 

Recuerde que la función pasada a componentFromStream debe devolver observable. Podemos lograr esto usando el operador of:



 ajax(url).pipe( pluck('response'), map(Component), catchError(error => of(<Error {...error} />)) ) 

Ahora nuestra interfaz de usuario se ve mucho mejor:



Indicador de carga


Es hora de presentar la gestión estatal. ¿De qué otra forma puede implementar el indicador de carga?


¿Qué pasa si place setState usaremos BehaviorSubject ?


Recomponer documentación sugiere lo siguiente:


En lugar de setState (), combine múltiples hilos

Ok, necesitas dos nuevas importaciones:


 import { BehaviorSubject, merge, of } from 'rxjs'; 

BehaviorSubject contendrá el estado de descarga y la merge lo asociará con el componente.


componentFromStream interno de componentFromStream :


 const User = componentFromStream(prop$ => { const loading$ = new BehaviorSubject(false); const getUser$ = ... 

Un BehaviorSubject inicializa con un valor inicial o "estado". Como no estamos haciendo nada hasta que el usuario comience a ingresar texto, inicialícelo en false .


Cambiaremos el estado de loading$ utilizando el operador de tap :


 import { catchError, debounceTime, filter, map, pluck, switchMap, tap // <--- } from 'rxjs/operators'; 

Lo usaremos así:


 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} />)) ) ) ); 

Justo antes de la solicitud switchMap y AJAX, pasamos true al valor de loading$ y false después de la respuesta exitosa.


Y ahora solo conectamos loading$ y getUser$ .


 return merge(loading$, getUser$).pipe( map(result => (result === true ? <h3>Loading...</h3> : result)) ); 

Antes de ver el trabajo, podemos importar la declaración de delay para que las transiciones no sean demasiado rápidas.


 import { catchError, debounceTime, delay, filter, map, pluck, switchMap, tap } from 'rxjs/operators'; 

Agregar delay antes del map(Component) :


 ajax(url).pipe( pluck('response'), delay(1500), map(Component), tap(() => loading$.next(false)), catchError(error => of(<Error {...error} />)) ) 

Resultado?



Todos :)

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


All Articles