Directrices prácticas para desarrollar aplicaciones React a gran escala. Parte 2: gestión del estado, enrutamiento

Hoy publicamos la segunda parte de la traducción del material, dedicada al desarrollo de aplicaciones React a gran escala. Aquí hablaremos sobre la gestión del estado de las aplicaciones, el enrutamiento y el desarrollo de interfaces.



Parte 1: Pautas prácticas para desarrollar aplicaciones React a gran escala. Planificación, acciones, orígenes de datos y API

Parte 2: Pautas prácticas para desarrollar aplicaciones React a gran escala. Parte 2: gestión del estado, enrutamiento


Administración del estado de la aplicación, integración de Redux, organización de enrutamiento


Aquí hablaremos sobre cómo puede ampliar la funcionalidad de Redux para poder realizar de manera ordenada operaciones complejas en la aplicación. Si tales mecanismos se implementan mal, pueden violar el patrón utilizado en el diseño del repositorio.

Las funciones del generador de JavaScript pueden resolver muchos de los problemas asociados con la programación asincrónica. El hecho es que estas funciones se pueden iniciar y detener a petición del programador. El middleware Redux-saga utiliza este concepto para gestionar aspectos problemáticos de una aplicación. En particular, estamos hablando de resolver tales problemas, que no pueden resolverse con la ayuda de reductores, presentados en forma de funciones puras.

▍Resolver tareas que no se pueden resolver con funciones puras


Considere el siguiente escenario. Le ofrecieron trabajar en una aplicación diseñada para una empresa que trabaja en el mercado inmobiliario. El cliente quiere obtener un sitio web nuevo y mejor. A su disposición hay una API REST, tiene diseños de todas las páginas preparadas con Zapier, ha esbozado un plan de aplicación. Pero luego surgió un problema considerable.

La empresa cliente ha estado utilizando un cierto sistema de gestión de contenido (CMS). Los empleados de la empresa conocen bien este sistema, por lo que el cliente no quiere cambiar al nuevo CMS solo para facilitar la escritura de nuevas publicaciones en el blog corporativo. Además, también debe copiar las publicaciones existentes del blog en un sitio nuevo, y esto también puede generar un problema.

Lo bueno es que el CMS utilizado por el cliente tiene una API conveniente a través de la cual puede acceder a publicaciones desde el blog. Pero, si creó un agente para trabajar con esta API, la situación se complica por el hecho de que se encuentra en un determinado servidor, en el que los datos no se presentan como necesita.

Este es un ejemplo de un problema, algo que podría contaminar el código de la aplicación, ya que aquí debe incluir en el proyecto los mecanismos para trabajar con la nueva API utilizada para descargar publicaciones de blog. Puede lidiar con esta situación con redux-saga.
Echa un vistazo al siguiente diagrama. Así es como interactúan nuestra aplicación y API. Descargamos publicaciones en segundo plano usando redux-saga.


Diagrama de aplicación con almacenamiento de Redux y redux-saga

Aquí, el componente distribuye la acción GET.BLOGS . La aplicación utiliza redux-saga, por lo que esta solicitud será interceptada. Después de eso, la función de generador descargará datos del almacén de datos en segundo plano y actualizará el estado de la aplicación compatible con Redux.

Aquí hay un ejemplo de cómo se vería la función del generador para cargar publicaciones (tales funciones se llaman "sagas") en la situación descrita. Las sagas se pueden usar en otros escenarios. Por ejemplo, para organizar el almacenamiento de datos del usuario (por ejemplo, pueden ser tokens), ya que este es otro ejemplo de una tarea para la que las funciones puras no son adecuadas.

 ... function* fetchPosts(action) { if (action.type === WP_POSTS.LIST.REQUESTED) {   try {     const response = yield call(wpGet, {       model: WP_POSTS.MODEL,       contentType: APPLICATION_JSON,       query: action.payload.query,     });     if (response.error) {       yield put({         type: WP_POSTS.LIST.FAILED,         payload: response.error.response.data.msg,       });       return;         yield put({       type: WP_POSTS.LIST.SUCCESS,       payload: {         posts: response.data,         total: response.headers['x-wp-total'],         query: action.payload.query,       },       view: action.view,     });   } catch (e) {     yield put({ type: WP_POSTS.LIST.FAILED, payload: e.message });  ... 

La saga presentada aquí espera acciones como WP_POSTS.LIST.REQUESTED . Al recibir tal acción, carga datos de la API. Ella, después de recibir los datos, envía otra acción: WP_POSTS.LIST.SUCCESS . Su procesamiento lleva a actualizar el repositorio utilizando el reductor apropiado.

▍ Introducción de reductores


Al desarrollar aplicaciones grandes, es imposible planificar de antemano el dispositivo de todos los modelos necesarios. Además, a medida que aumenta el tamaño de la aplicación, el uso de la tecnología de introducción de reductores ayuda a ahorrar una gran cantidad de horas hombre. Esta técnica permite a los desarrolladores agregar nuevos reductores al sistema sin tener que volver a escribir todo el repositorio.

Hay bibliotecas que están diseñadas para crear repositorios dinámicos de Redux. Sin embargo, prefiero el mecanismo de introducción de reductores, ya que le da al desarrollador un cierto nivel de flexibilidad. Por ejemplo, una aplicación existente puede equiparse con este mecanismo sin la necesidad de reorganizar seriamente la aplicación.

La introducción de reductores es una forma de separación de código. La comunidad de desarrolladores de React está adoptando con entusiasmo esta tecnología. Usaré este fragmento de código para demostrar la apariencia y las características del mecanismo de implementación de los reductores.

Primero, veamos su integración con Redux:

 ... const withConnect = connect( mapStateToProps, mapDispatchToProps, ); const withReducer = injectReducer({ key: BLOG_VIEW, reducer: blogReducer, }); class BlogPage extends React.Component {  ... } export default compose( withReducer, withConnect, )(BlogPage); 

Este código es parte del archivo BlogPage.js que contiene el componente de la aplicación.

Aquí, en el comando de exportación, no utilizamos la función de connect , sino la función de compose . Esta es una de las funciones de la biblioteca Redux que le permite componer varias funciones. La lista de funciones pasadas para compose debe leerse de derecha a izquierda o de abajo hacia arriba.

Puede aprender de la documentación de Redux que la función de compose permite crear transformaciones de funciones profundamente anidadas. En este caso, el programador se libera de la necesidad de usar estructuras muy largas. Estas construcciones se ven como líneas de código que representan llamadas a algunas funciones al pasarles los resultados de llamadas a otras funciones como argumentos. La documentación señala que la función de compose debe usarse con precaución.

La función más a la derecha de una composición puede tomar muchos argumentos, pero solo se puede pasar un argumento a las funciones que la siguen. Como resultado, al llamar a la función que resultó del uso de compose , le pasamos lo que se necesita de las funciones originales, que está a la derecha de todas las demás. Es por eso que pasamos la función componer a la función withConnect como último parámetro. Como resultado, la función de compose se puede usar al igual que la función de connect .

▍ Enrutamiento y Redux


Hay una serie de herramientas que se utilizan para resolver problemas de enrutamiento en las aplicaciones. En esta sección, sin embargo, nos centraremos en la biblioteca react-router-dom . Ampliaremos sus capacidades para que pueda funcionar con Redux.

Muy a menudo, el enrutador React se usa de esta manera: el componente raíz está encerrado en la etiqueta BrowserRouter , y los contenedores secundarios se envuelven en el método withRouter() y se exportan ( aquí hay un ejemplo).

Con este enfoque, el componente secundario recibe, a través del mecanismo de props , un objeto de history que contiene algunas propiedades específicas de la sesión de usuario actual. Hay algunos métodos en este objeto que pueden usarse para controlar la navegación.

Esta opción de enrutamiento puede generar problemas en aplicaciones grandes. Esto se debe al hecho de que no tienen un objeto de history centralizado. Además, los componentes que no se representan con <Route> no pueden funcionar con el objeto de history . Aquí hay un ejemplo usando <Route> :

 <Route path="/" exact component={HomePage} /> 

Para resolver este problema, usaremos la biblioteca conectado-reaccionar-enrutador , que nos permitirá establecer el enrutamiento utilizando el método de dispatch . La integración de esta biblioteca en el proyecto requerirá algunas modificaciones. En particular, será necesario crear un nuevo reductor diseñado específicamente para rutas (esto es bastante obvio), y también agregar algunos mecanismos auxiliares al sistema.

Después de completar su configuración, el nuevo sistema de enrutamiento se puede utilizar a través de Redux. Por lo tanto, la navegación en la aplicación se puede implementar enviando acciones.

Para aprovechar las capacidades de la biblioteca de enrutadores de reacción conectada en el componente, simplemente asignamos el método de dispatch al repositorio, haciendo esto de acuerdo con las necesidades de la aplicación. Aquí hay un ejemplo de código que demuestra el uso de la biblioteca conectado-reaccionar-enrutador (para que este código funcione, necesita que el resto del sistema esté configurado para usar conectado-reaccionar-enrutador).

 import { push } from 'connected-react-router' ... const mapDispatchToProps = dispatch => ({  goTo: payload => {    dispatch(push(payload.path));  }, }); class DemoComponent extends React.Component {  render() {    return (      <Child        onClick={          () => {            this.props.goTo({ path: `/gallery/`});                      />    } ... 

Aquí, el método goTo distribuye una acción que empuja la URL requerida goTo pila del historial goTo navegador. Anteriormente, el método goTo se goTo al repositorio. Por lo tanto, este método se pasa a DemoComponent en el objeto props .

Interfaz de usuario dinámica y necesidades de una aplicación en crecimiento.


Con el tiempo, a pesar de la presencia de un back-end adecuado para la aplicación y una parte del cliente de alta calidad, algunos elementos de la interfaz de usuario comienzan a afectar negativamente el trabajo del usuario. Esto se debe a la implementación irracional de los componentes, que, a primera vista, parece muy simple. En esta sección, discutiremos las recomendaciones para crear algunos widgets. Su implementación correcta, a medida que la aplicación crece, se vuelve más complicada.

▍ Carga diferida y reacción.


La mejor parte de la naturaleza asincrónica de JavaScript es que aprovecha todo el potencial del navegador. Quizás el verdadero bien es que para comenzar un determinado proceso no es necesario esperar a que se complete la tarea anterior. Sin embargo, los desarrolladores no pueden influir en la red y la velocidad con la que se cargan los diversos materiales necesarios para el funcionamiento de los sitios.

Los subsistemas de red generalmente se perciben como poco confiables y propensos a errores.

El desarrollador, en un esfuerzo por hacer que su aplicación sea lo más alta posible, puede someterla a muchas comprobaciones y lograr su aprobación exitosa. Pero aún hay algunas cosas, como el estado de la conexión de red o el tiempo de respuesta del servidor, que el desarrollador no puede influir.

Pero los creadores del software no buscan justificar el trabajo de baja calidad de las aplicaciones con frases como "este no es mi negocio". Encontraron formas interesantes de lidiar con problemas de red.

En algunas partes de la aplicación front-end, es posible que deba mostrar algunos materiales de respaldo (como cargar mucho más rápido que los materiales reales). Esto evitará que el usuario contemple la "contracción" de las páginas de carga o, lo que es peor, sobre tales íconos.


Es mejor que los usuarios no vean algo así.

La tecnología React Suspense le permite hacer frente a esos problemas. Por ejemplo, le permite mostrar un determinado indicador durante la carga de datos. Aunque esto también se puede hacer manualmente estableciendo la propiedad isLoaded en true , el uso de la API Suspense hace que el código sea mucho más limpio.

Aquí puede ver un buen video sobre Suspenso, en el que Jared Palmer presenta al público esta tecnología y muestra algunas de sus características utilizando un ejemplo de una aplicación real .

Así es como funciona la aplicación sin usar Suspenso.


Aplicación en la que no se utiliza suspenso

Equipar un componente con soporte de Suspenso es mucho más fácil que usar isLoaded toda la isLoaded . Comencemos colocando el contenedor de la App principal en React.StrictMode . Nos aseguramos de que entre los módulos React utilizados en la aplicación, no haya aquellos que se consideren obsoletos.

 <React.Suspense fallback={<Spinner size="large" />}>  <ArtistDetails id={this.props.id}/>  <ArtistTopTracks />  <ArtistAlbums id={this.props.id}/> </React.Suspense> 

Los componentes envueltos en etiquetas React.Suspense , durante la carga del contenido principal, cargan y muestran lo que se especifica en la propiedad de fallback . Debemos esforzarnos por garantizar que los componentes utilizados en la propiedad de fallback tengan el volumen más pequeño posible y estén organizados de la manera más simple posible.


Aplicación que utiliza suspenso

▍ Componentes adaptativos


En aplicaciones frontales grandes, la manifestación de patrones repetitivos es común. Al mismo tiempo, al comienzo del trabajo, esto puede ser casi completamente obvio. No hay nada que hacer al respecto, pero debe haber encontrado esto.

Por ejemplo, hay dos modelos en la aplicación. Uno de ellos está destinado a la descripción de pistas de carreras, y el segundo, a la descripción de automóviles. La página de la lista de autos usa elementos cuadrados. Cada uno de ellos contiene una imagen y una breve descripción.

La lista de rastreo utiliza elementos similares. Su característica principal es que, además de la imagen y la descripción de la ruta, también tienen un pequeño campo que indica si es posible que los espectadores de una carrera en esta ruta compren algo para comer.


Elemento para la descripción del automóvil y elemento para la descripción de la pista.

Estos dos componentes son ligeramente diferentes entre sí en términos de estilo (tienen diferentes colores de fondo). El componente que describe la ruta contiene información adicional sobre el objeto del mundo real que describe, mientras que el componente que simboliza el automóvil no tiene dicha información. Este ejemplo muestra solo dos modelos. En una aplicación grande, se pueden escribir muchos modelos similares, que solo difieren en cosas pequeñas.

La creación de componentes independientes separados para cada una de estas entidades es contraria al sentido común.

Un programador puede ahorrarse la necesidad de escribir fragmentos de código que se repitan casi por completo. Esto se puede hacer mediante el desarrollo de componentes adaptativos. Ellos, en el curso del trabajo, tienen en cuenta el entorno en el que se cargaron. Considere la barra de búsqueda de una determinada aplicación.


Barra de búsqueda

Se usará en muchas páginas. Al mismo tiempo, se realizarán cambios menores en su apariencia y el orden de su trabajo en diferentes páginas. Por ejemplo, en la página de inicio del proyecto, será un poco más grande que en otras páginas. Para resolver este problema, puede crear un solo componente que se mostrará de acuerdo con las propiedades que se le transfieran.

 static propTypes = {  open: PropTypes.bool.isRequired,  setOpen: PropTypes.func.isRequired,  goTo: PropTypes.func.isRequired, }; 

Con esta técnica, puede controlar el uso de clases HTML al representar dichos elementos, lo que le permite influir en su apariencia.

Otra situación interesante en la que los componentes adaptativos pueden encontrar aplicación es el mecanismo para dividir ciertos materiales en páginas. Una barra de navegación puede estar presente en cada página de la aplicación. Las instancias de este panel en cada una de las páginas serán casi exactamente las mismas que en las otras páginas.


Panel de paginación

Supongamos que cierta aplicación necesita un panel similar. Al trabajar en esta aplicación, los desarrolladores se adhieren a los requisitos articulados oportunos. En tal situación, el componente adaptativo utilizado para dividir el material en páginas solo necesita pasar un par de propiedades. Esta es la URL y el número de elementos por página.

Resumen


El ecosistema React se ha vuelto tan maduro en estos días que es poco probable que alguien necesite "inventar una bicicleta" en cualquier etapa del desarrollo de la aplicación. Aunque esto juega en manos de los desarrolladores, conduce al hecho de que se hace difícil elegir exactamente qué funciona bien para cada proyecto específico.

Cada proyecto es único en términos de su alcance y funcionalidad. No existe un enfoque único o una regla universal en el desarrollo de aplicaciones React. Por lo tanto, antes de comenzar el desarrollo, es importante planificarlo correctamente.

Al planificar, es muy fácil entender qué herramientas se crean directamente para el proyecto y cuáles claramente no son adecuadas para él, ya que son demasiado grandes para él. Por ejemplo, una aplicación que consta de 2-3 páginas y realiza muy pocas solicitudes a ciertas API no necesita almacenes de datos similares a los que mencionamos. Estoy listo para ir aún más lejos en estas consideraciones y decir que en proyectos pequeños no es necesario usar Redux.
En la etapa de planificación de la aplicación, al dibujar diseños de sus páginas, es fácil ver que se utilizan muchos componentes similares en estas páginas. Si intenta reutilizar el código de dichos componentes o se esfuerza por escribir componentes universales, esto ayudará a ahorrar mucho tiempo y esfuerzo.

Y finalmente, me gustaría señalar que los datos son el núcleo de cualquier aplicación. Y las aplicaciones React no son una excepción. A medida que crece la escala de la aplicación, aumentan los volúmenes de datos procesados, aparecen mecanismos de software adicionales para trabajar con ellos. Algo así, si la aplicación fue mal diseñada, puede fácilmente "aplastar" a los programadores, llenándolos de tareas complejas y confusas. Si, en el curso de la planificación, los problemas del uso de los almacenes de datos se decidieran de antemano, si el orden de trabajo de las acciones, los reductores y el hundimiento se pensaran de antemano, trabajar en la aplicación sería mucho más fácil.

Estimados lectores! Si conoce alguna biblioteca o metodología de desarrollo que funcione bien al crear aplicaciones React a gran escala, compártalas.

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


All Articles