El autor del material, cuya traducción publicamos hoy, dice que aquí quiere demostrar el proceso de desarrollo de una aplicación React simple que utiliza RxJS. Según él, él no es un experto en RxJS, ya que él mismo está estudiando esta biblioteca y no rechaza la ayuda de personas conocedoras. Su objetivo es llamar la atención de la audiencia sobre formas alternativas de crear aplicaciones React, para inspirar al lector a hacer una investigación independiente. Este material no se puede llamar una introducción a RxJS. Aquí se mostrará una de las muchas formas de usar esta biblioteca en el desarrollo de React.

Como empezó todo
Recientemente,
mi cliente me inspiró a aprender sobre el uso de
RxJS para administrar el estado de las aplicaciones React. Cuando audité el código de la aplicación para este cliente, quería saber mi opinión sobre cómo desarrolló la aplicación, dado que antes usaba exclusivamente el estado React local. El proyecto llegó a un punto en el que no era razonable confiar únicamente en React. Primero, hablamos sobre el uso de
Redux o MobX como un mejor medio para administrar el estado de su aplicación. Mi cliente creó un prototipo para cada una de estas tecnologías. Pero no se detuvo en estas tecnologías, creando un prototipo de una aplicación React que usa RxJS. A partir de ese momento, nuestra conversación se volvió mucho más interesante.
La aplicación en cuestión era una plataforma de negociación de criptomonedas. Tenía muchos widgets cuyos datos se actualizaron en tiempo real. Los desarrolladores de esta aplicación, entre otros, tuvieron que resolver las siguientes tareas difíciles:
- Administre múltiples solicitudes de datos (asíncronas).
- Actualización en tiempo real de una gran cantidad de widgets en el panel de control.
- La solución al problema de los widgets y la conectividad de datos, ya que algunos widgets necesitaban datos no solo de algunas fuentes especiales, sino también de otros widgets.
Como resultado, las principales dificultades que enfrentaron los desarrolladores no estaban relacionadas con la biblioteca React en sí, y además, podría ayudarlos en esta área. El principal problema era hacer que los mecanismos internos del sistema funcionaran correctamente, los que vinculaban los datos de la criptomoneda y los elementos de la interfaz creados por las herramientas React. Fue en esta área que las capacidades de RxJS resultaron ser muy útiles, y el prototipo que me mostraron parecía muy prometedor.
Usando RxJS en React
Supongamos que tenemos una aplicación que, después de realizar algunas acciones locales, realiza solicitudes a una
API de terceros . Te permite buscar por artículo. Antes de ejecutar la solicitud, necesitamos obtener el texto que se utiliza para formar esta solicitud. En particular, nosotros, usando este texto, formamos una URL para acceder a la API. Aquí está el código del componente React que implementa esta funcionalidad
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 carece de un sistema de gestión de estado. El estado de la propiedad de
query
no se almacena en ningún lado, la función
onChangeQuery
tampoco actualiza el estado. En el enfoque convencional, dicho componente está equipado con un sistema de gestión estatal local. Se ve así:
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:
Sin embargo, este no es el enfoque del que vamos a hablar aquí. En cambio, queremos establecer un sistema de gestión de estado de la aplicación utilizando RxJS. Veamos cómo hacer esto usando Componente de orden superior (HOC).
Si lo desea, puede implementar una lógica similar en el componente de su
App
, pero lo más probable es que, en algún momento de su trabajo en la aplicación, decida diseñar dicho componente en forma de un
HOC que sea adecuado para su reutilización.
Componentes React y RxJS de primer orden
Descubriremos cómo administrar el estado de las aplicaciones React utilizando RxJS, utilizando un componente de orden superior para este propósito. En cambio, uno podría implementar
la plantilla de accesorios de renderizado. Como resultado, si no desea crear un componente de orden superior para este propósito, puede utilizar los componentes de orden superior observables
Recompose with
mapPropsStream()
y
componentFromStream()
. En esta guía, sin embargo, haremos todo por nuestra cuenta.
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);
Mientras que el componente de orden superior RxJS no realiza ninguna acción. Solo transfiere su propio estado y propiedades al componente de entrada, que se planea expandir con su ayuda. Como puede ver, al final, un componente de orden superior estará involucrado en la gestión del estado de React. Sin embargo, este estado se obtendrá del flujo observado. Antes de comenzar a implementar HOC y usarlo con el componente de la
App
, necesitamos instalar RxJS:
npm install rxjs
Ahora comencemos a usar un componente de orden superior e implementemos su 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);
El componente de la
App
sí no cambia. Acabamos de pasar dos argumentos a un componente de orden superior. Los describimos:
- Objeto observado El argumento de
query
es un objeto observable que tiene un valor inicial, pero, además, devuelve, con el tiempo, nuevos valores (ya que este es un BehaviorSubject
). Cualquiera puede suscribirse a este objeto observable. Esto es lo que dice la documentación de RxJS sobre los objetos de tipo BehaviorSubject
: "Una de las opciones para los objetos Subject
es un BehaviorSubject
que utiliza el concepto de" valor actual ". Almacena el último valor pasado a sus suscriptores, y cuando un nuevo observador se suscribe a él, inmediatamente recibe este "valor actual" del BehaviorSubject
. Tales objetos son muy adecuados para presentar datos, nuevas porciones de las cuales aparecen con el tiempo ". - El sistema para emitir nuevos valores para el objeto observado (disparador). La función
onChangeQuery()
, pasada a través del HOC al componente de la App
, es una función regular que pasa el siguiente valor al objeto observado. Esta función se transfiere en el objeto, ya que puede ser necesario transferir a un componente de orden superior varias funciones que realizan ciertas acciones con los objetos observados.
Después de crear el objeto observado y suscribirse a él, el flujo de la solicitud debería funcionar. Sin embargo, hasta ahora, el componente de orden superior en sí mismo nos parece un cuadro negro. Nos damos cuenta de eso:
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} /> ); } }; };
Un componente de orden superior recibe un objeto observable y un objeto con disparadores (quizás este objeto que contiene funciones se puede llamar un término más exitoso del léxico RxJS), representado en la firma de la función.
Los disparadores solo se pasan a través del HOC al componente de entrada. Es por eso que el componente de la
App
recibe directamente la función
onChangeQuery()
, que trabaja directamente con el objeto observado, pasándole nuevos valores.
El objeto observado utiliza el método del ciclo de vida del
componentDidMount()
para firmar y el método del
componentDidMount()
para darse de baja. La cancelación de la suscripción es necesaria para evitar
pérdidas de memoria . En la suscripción del objeto observado, la función solo envía todos los datos entrantes de la secuencia al almacén de estado React local utilizando el
this.setState()
.
Hagamos un pequeño cambio en el componente de la
App
, que eliminará el problema que ocurre si el componente de orden superior no establece el valor inicial para la propiedad de
query
. Si esto no se hace, entonces, al comienzo del trabajo, la propiedad de
query
será
undefined
. Gracias a este cambio, esta propiedad obtiene el valor predeterminado.
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> );
Otra forma de lidiar con este problema es establecer el estado inicial de la
query
en un componente de orden 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);
Si prueba esta aplicación ahora, el cuadro de entrada debería funcionar como se esperaba. El componente de la
App
recibe del HOC, en forma de propiedades, solo el estado de la
query
y la función
onChangeQuery
para cambiar el estado.
La recuperación y el cambio de estado se producen a través de objetos observables RxJS, a pesar del hecho de que se utiliza un almacén interno de estado React dentro del componente de orden superior. No pude encontrar una solución obvia al problema de la transmisión de datos desde una suscripción de objetos observados directamente a las propiedades del componente extendido (
App
). Es por eso que tuve que usar el estado local de React en forma de una capa intermedia, que, además, es conveniente en términos de las causas de la nueva representación. Si conoce otra forma de lograr los mismos objetivos, puede compartirla en los comentarios.
Combinando objetos observados en React
Creemos una segunda secuencia de valores, que, como la propiedad de
query
, se puede usar en el componente
App
. Más tarde usaremos ambos valores, trabajando con ellos usando otro objeto observable.
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 puede ver, el parámetro
subject
se puede usar para refinar la solicitud al construir la URL utilizada para acceder a la API. Es decir, los materiales se pueden buscar en función de su popularidad o en la fecha de publicación. A continuación, cree otro objeto observable que pueda usarse para cambiar el parámetro
subject
. Este observable se puede usar para asociar un componente de la
App
con un componente de orden superior. De lo contrario, las propiedades pasadas al componente de la
App
no funcionarán.
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);
El
onSelectSubject()
no es nuevo. A través de un botón, se puede usar para cambiar entre dos estados
subject
. Pero el objeto observado transferido a un componente de orden superior es algo nuevo. Utiliza la función
combineLatest()
de RxJS para combinar los últimos valores devueltos de dos (o más) secuencias observadas. Después de suscribirse al objeto observado, si se modifica alguno de los valores (
query
o
subject
), el suscriptor recibirá ambos valores.
Un complemento al mecanismo implementado por la función
combineLatest()
es su último argumento. Aquí puede establecer el orden de retorno de los valores generados por los objetos observados. En nuestro caso, necesitamos que estén representados como un objeto. Esto permitirá, como antes, desestructurarlos en un componente de orden superior y escribirlos en el estado React local. Como ya tenemos la estructura necesaria, podemos omitir el paso de envolver el objeto del 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);
El objeto fuente,
{ query: '', subject: 'search' }
, así como todos los demás objetos devueltos por el flujo combinado de objetos observados, son adecuados para desestructurarlos en un componente de orden superior y para escribir los valores correspondientes en el estado React local. Después de actualizar el estado, como antes, se realiza la representación. Cuando inicie la aplicación actualizada, debería poder cambiar ambos valores usando el campo de entrada y el botón. Los valores modificados afectan la URL utilizada para acceder a la API. Incluso si solo uno de estos valores cambia, el otro valor conserva su último estado, ya que la función
combineLatest()
siempre combina los valores más recientes emitidos por los flujos observados.
Axios y RxJS en React
Ahora en nuestro sistema, la URL para acceder a la API se construye en base a dos valores de un objeto observable combinado, que incluye otros dos objetos observables. En esta sección, utilizaremos la URL para cargar datos desde la API. Es posible que pueda usar el
sistema de carga de datos React , pero cuando use objetos observables RxJS, debe agregar otra secuencia observable a nuestra aplicación.
Antes de comenzar a trabajar en el siguiente objeto observado, instale
axios . Esta es la biblioteca que usaremos para cargar datos de secuencias en el programa.
npm install axios
Ahora imagine que tenemos una serie de artículos que el componente de la
App
debería generar. Aquí, como el valor del parámetro predeterminado correspondiente, usamos una matriz vacía, haciendo lo mismo que hicimos con otros 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 artículo de la lista, se utiliza un valor de reserva debido al hecho de que la API a la que estamos accediendo no es uniforme. Ahora, lo más interesante, la implementación de un nuevo objeto observable que es responsable de cargar datos en una aplicación React que los visualizará.
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), ); ...
Un nuevo objeto observable es, nuevamente, una combinación de
subject
y
query
observables, ya que, para construir la URL con la que accederemos a la API para cargar datos, necesitamos ambos valores. En el método
pipe()
del objeto observado, podemos usar los llamados "operadores RxJS" para realizar ciertas acciones con los valores. En este caso, asignamos dos valores que se colocan en la consulta, que axios usa para obtener el resultado. Aquí usamos el operador
flatMap()
y no
map()
para acceder al resultado de la promesa resuelta con éxito, y no a la promesa devuelta en sí. Como resultado, después de suscribirse a este nuevo objeto observable, cada vez que se ingresa un nuevo
subject
o valor de
query
en el sistema desde otros objetos observables, se ejecuta una nueva
query
y el resultado está en la función de suscripción.
Ahora, nuevamente, podemos proporcionar un nuevo observable a un componente de orden superior. Tenemos el último argumento para la función
combineLatest()
nuestra disposición, esto hace posible
combineLatest()
directamente a una propiedad llamada
stories
. Al final, esto representa cómo estos datos ya se están utilizando en el componente de la
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)
Aquí no hay disparador, ya que el objeto observado se activa indirectamente por otros dos flujos observados. Cada vez que se cambia el valor en el campo de entrada (
query
) o se hace clic en el botón (
subject
), esto afecta al objeto de
fetch
observado, que contiene los últimos valores de ambas secuencias.
Sin embargo, quizás no necesitemos que cada vez que cambiemos el valor en el campo de entrada, esto afecte el objeto de
fetch
observado. Además, no desearíamos que la
fetch
se vea afectada si el valor es una cadena vacía. Es por eso que podemos extender el objeto de
query
observable utilizando la instrucción de
debounce
, que elimina los cambios de consulta demasiado frecuentes. Es decir, gracias a este mecanismo, un nuevo evento se acepta solo después de un tiempo predeterminado después del evento anterior. Además, aquí utilizamos el operador de
filter
, que filtra los eventos de flujo si la cadena de
query
está vacía.
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), ); ...
La declaración
debounce
hace el trabajo de ingresar datos en el campo. Sin embargo, cuando un botón hace clic en el valor del
subject
, la solicitud debe ejecutarse inmediatamente.
Ahora, los valores iniciales para la
query
y el
subject
, que vemos cuando el componente de la
App
se muestra por primera vez, no son los mismos que los obtenidos a partir de los valores iniciales de los objetos observados:
const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY);
subject
escribe en el
subject
, una cadena vacía está en
query
. Esto se debe al hecho de que fueron estos valores los que proporcionamos como parámetros predeterminados para reestructurar la función del componente de la
App
en la firma. La razón de esto es porque necesitamos esperar la solicitud inicial ejecutada por el objeto
fetch
observable. Como no sé exactamente cómo obtener valores inmediatamente de
query
observables y objetos
subject
en un componente de orden superior para escribirlos en un estado local, decidí configurar nuevamente el estado inicial para el componente de orden 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} /> ); } }; };
Ahora el estado inicial se puede proporcionar a un componente de orden superior como tercer argumento. En el futuro, podemos eliminar la configuración predeterminada para el componente de la
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);
Lo que me preocupa en este momento es que el estado inicial también se establece en la declaración de los objetos observados
query$
y
subject$
. Este enfoque es propenso a errores, ya que la inicialización de los objetos observados y el estado inicial del componente de orden superior comparten los mismos valores. Me hubiera gustado más si, en cambio, los valores iniciales se extrajeran de los objetos observados en un componente de orden superior para establecer el estado inicial. Quizás uno de los lectores de este material pueda compartir consejos en los comentarios sobre cómo hacer esto.
El código para el proyecto de software que hicimos aquí se puede encontrar
aquí .
Resumen
El objetivo principal de este artículo es demostrar un enfoque alternativo para desarrollar aplicaciones React usando RxJS. Esperamos que te haya dado algo para pensar. A veces, Redux y MobX no son necesarios, pero quizás en tales situaciones, RxJS sea justo lo que es adecuado para un proyecto específico.
Estimados lectores! ¿Utiliza RxJS cuando desarrolla aplicaciones React?
