Uso de RxJS en React Development para administrar el estado de la aplicación

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.

imagen

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://hn.algolia.com/api/v1/search?query=${         this.state.query       }`}</p>     </div>   ); } } export default App; 

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 --save 

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 --save 

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?

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


All Articles