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