Como ya explicamos en este artículo, el patrón REDUX nos proporciona muchas ventajas a la hora de mantener y gestionar aplicaciones de frontend con un cierto grado de complejidad. Aquí vamos a ver cómo podemos implementar este patrón y cuáles son sus componentes para uno de los frameworks de JavaScript más populares a día de hoy: Angular.
REDUX con Angular: consideraciones iniciales
Para implementar REDUX con Angular existe un estándar de facto, se trata de la librería Ngrx. Esta librería nos proporciona todos los componentes que vamos a necesitar para implementar con éxito este patrón y la gestión de estados.
Otra cosa que debemos tener en cuenta es que la aplicación de REDUX va a añadir una capa de complejidad importante a nuestra app. Este hecho hace que sea recomendable para cierto tipo de aplicaciones web que ya tengan cierta entidad. Por lo tanto, es probable que no merezca la pena implementarlo para, por ejemplo, un proyecto pequeño en el que únicamente se van a mostrar cuatro vistas con tablas que recogen datos que vienen de una API. Este es un factor importante que hay que tener en cuenta a la hora de incluir la implementación de REDUX en un proyecto.
Flujo de datos
En el siguiente esquema vemos el flujo que van a seguir los datos y los distintos componentes que formarán parte de nuestra implementación:
Elementos en nuestra implementación:
Component
Si se ha llegado a este artículo, probablemente se conozcan ya los componentes de Angular, por lo que no vamos a extendernos demasiado. Un componente es el elemento que prepara los datos para una determinada vista en nuestra aplicación, es la forma en la que Angular estructura un proyecto. Consta de un archivo typescript que actúa como controlador y una vista en HTML. Como se puede ver, seguimos el patrón MVC (Model View Controller) de organización de proyectos web.
State
Este elemento es la clave de toda la organización de datos. Aquí es donde se almacenan los datos para nuestra aplicación. Normalmente el estado lo tendremos modularizado en distintos modelos, tales como un modelo Usuario o un modelo Rol, que reflejan los distintos modelos de datos que va a necesitar nuestra aplicación. En definitiva, es la información que vamos a mostrar al usuario de la aplicación web pero organizada en el Store.
Cabe destacar que siguiendo el paradigma de que el store es la única fuente de verdad, los componentes solo van a obtener la información de este State y no podrán recibirla de ningún otro sitio, ya que dejaríamos de aplicar REDUX si lo hiciéramos.
Action
Siguiendo la implementación, para realizar cualquier cosa, un componente va siempre a desencadenar una acción. Ya sea solicitar un listado de usuarios o la modificación de un registro, el componente lo que hará será simplemente notificar al Store que quiere llevar a cabo esa acción despachando esta Action y, a partir de ahí, el Store se encargará de realizar las operaciones que sean necesarias para llevarla a cabo y crear una nueva versión del estado de la aplicación.
Estas actions podríamos considerarlas los mensajeros que envían el componente al Store para especificar qué quiere hacer.
Reducer
En este punto es importante recordar que el estado de nuestra aplicación es inmutable. Esto implica que cuando cambia algún dato, en realidad lo que hacemos es sustituir el estado por uno nuevo que incorpora los cambios.
Debido a esto, existen los reducers. Son funciones puras cuya única función es sustituir el estado de la aplicación antiguo por un estado nuevo con los cambios que se hayan introducido.
Los reducers son además el único elemento autorizado a modificar el estado de la aplicación, son el único canal por el que podemos sustituir un estado previo por el nuevo estado.
Effect
Un aspecto importante que hemos mencionado de pasada es que los reducers son funciones puras, por lo que no pueden hacer nada que no sea tomar como argumento el estado previo e incluir los cambios en el nuevo estado que estamos creando.
Esta aproximación nos deja varias situaciones que no podemos controlar bien, como el hecho de que el cambio que debemos hacer en el Store dependa de la respuesta que obtenemos del servidor, un caso muy común por otra parte. Esto no puede hacerlo un reducer, un reducer no puede tomar decisiones de este tipo.
Aquí es donde los effects pueden ayudarnos. Un effect es una función asociada a un Action que nos ayudará a realizar todas aquellas tareas auxiliares que necesitemos. Este elemento nos da una gran flexibilidad a la hora de gestionar nuestro flujo de datos.
Fijándonos en el esquema del flujo de NgRx, los effects son los encargados de gestionar la comunicación con los servicios de nuestra app, siendo, a su vez, los elementos que gestionan la comunicación con las distintas fuentes de datos.
Ejemplo de obtención de datos con REDUX
Ahora que hemos explicado los distintos componentes, vamos a explicar con un ejemplo como interactúan entre ellos y la secuencia que se desencadena.
Suponiendo que entramos a la pantalla que nos muestra el listado de usuarios, este sería el ciclo completo de obtención de los datos:
- La función ngOnInit de nuestro componente despachará una action, por ejemplo getUserList().
- Esta action, getUserList, aún no puede cambiar el store. En realidad, esta primera action lo que tiene que hacer es solicitar esta información al API correspondiente, por lo que no tendrá asociado un reducer, tendrá asociado un effect.
- Este effect, pedirá al servicio de comunicación la información correspondiente del API que la contenga y esperará a recibir la respuesta del servidor.
- Mientras tanto, paralelamente, nuestra vista tendrá unos valores para estos users en el store, ya sean los últimos usuarios o una serie de valores por defecto. Aunque no estén actualizados, no tendremos un spinner bloqueando la vista lo que mejorará la experiencia del usuario.
- Volviendo al effect, una vez que este ha recibido la información del servidor, caben dos posibilidades: se reciben los usuarios de forma correcta o se recibe un error desde el servidor (error de autenticación, fallo de conexión, etc.). Por lo tanto, este efecto, en función de la respuesta que reciba, despachará una acción u otra dentro del propio efecto. No solo los componentes pueden despachar acciones, podemos despachar acciones en prácticamente cualquier lugar de nuestra app.
- En este punto, cada una de esas acciones, en función de la respuesta, tendrá un reducer asociado que modificará el store de una manera u otra.
- El estado de la aplicación acaba de cambiar dado que el reducer ha creado un nuevo estado y sustituido el anterior.
- Nuestra vista, que está suscrita al estado mediante un observable, recibirá los cambios en el store en tiempo real. El usuario no hará nada, simplemente irán cambiando los datos en función de la respuesta o mostrando algún error.
Así completamos el ciclo de obtención de datos. Este es un ejemplo sencillo, pero imaginemos la opción de que esa misma vista reciba datos de varias APIs diferentes y un servidor de WebSockets que sirve notificaciones en tiempo real desde el servidor. En este caso, se vuelve mucho más difícil de gestionar y cuando tenemos que mezclar datos de distintas APIs se empieza a disparar el tiempo de espera del usuario mientras generamos la vista.
Como vemos, y repitiendo la conclusión del artículo anterior (por qué usar REDUX en un proyecto frontend), REDUX no nos acelera el proceso de obtención de datos, sino que nos permite controlar, organizar y mantener los datos que usa nuestra aplicación de forma muy eficiente. Esto hace que sea mucho más sencilla de mantener y evolucionar cuando la aplicación va alcanzando cierta complejidad.
Además, como ya hemos comprobado, mejora muchísimo la experiencia de usuario, una de las claves para hacer que nuestra app o plataforma sea sencilla de manejar.