Curso online avanzado con Angular. Cupón descuento: 💸-20€
Continuando con el tutorial de Angular Avanzado nos centramos ahora en una arquitectura de comunicación de datos conocida como Unidirectional Data Flow o flujo de datos en un mismo sentido; la base del patrón Redux. Esta técnica es una mejora sobre el modelo básico de Angular, el double-binding, el cual facilitaba mucho el desarrollo en pequeños proyectos.
Cuando hablamos de mejora debemos ser honestos con los costes y beneficios: aquí vamos a mejorar la eficiencia en ejecución y a facilitar la depuración a costa de una mayor complejidad estructural y sintáctica. Merece la pena cuanto más grande sea el proyecto. Este es un ejemplo simplificado pero realista que implementa Redux. Tómate tu tiempo para estudiarlo con calma.
Tomar las decisiones correctas en cuestiones de este calibre puede suponer la diferencia entre el éxito o fracaso de un proyecto. Voy a explicarte las razones para usar este patrón y la manera más sencilla de introducirlo en tus aplicaciones, dejándote en el umbral de soluciones aún más potentes como la gestión de estado centralizada con Redux.
Partiendo de la aplicación tal como quedó en Componentes dinámicos, directivas y pipes con Angular. Al finalizar dotaremos a la aplicación de un almacén de datos Redux que notifica cambios reactivos.
Código asociado a este tutorial en GitHub: AcademiaBinaria/angular-boss
Tabla de Contenido:
1. Arquitectura del patrón Redux.
2. Implementación de un Store con RxJs.
1. Arquitectura del patrón Redux
1 | Como: desarrollador, |
Hablar de Unidirectional Data Flow sin presentar Redux es poco menos que imposible. Vamos a ver en qué consiste este patrón tan usado, no sólo en Angular. Lo que aprendas te servirá también para react, vue y otros.
Redux se ocupa de la gestión del estado, es decir, del valor de las variables en un determinado momento. Centraliza sus cambios para saber qué ocurrió para llegar a este valor, y qué valor tendrá según lo que ocurra. Desacopla los emisores de acciones de los receptores de información, para ello combina otros dos patrones:
Action: Envío de comandos al almacén para actualizar el estado
Observable: Subscripción a cambios en el estado del almacén.
Como incorpora cierta complejidad de serie conviene que nos hagamos esta pregunta ¿Cuándo usarlo?. Una pequeña lista de ideas sobre cuándo implementar Redux.
- Aplicaciones complejas con múltiples componentes.
- El patrón contenedor / presentadores se hace complejo y tedioso a partir de tres niveles de profundidad del árbol.
- Los
router-outlets
inician una jerarquía propia. Comunicar componentes entre árboles no es factible con@Input
y@Output
- Uso de estrategias de detección del cambio OnPush.
- Consumo de datos mediante pipes async suscritos a observables.
- Cambios concurrentes por el usuario y los servicios.
- Peticiones mediante pull a intervalos regulares
- Recepción de sockets
- Llamadas en paralelo
- Gestión de datos local con cachés
Redux no hace rápido lo simple, sino mantenible lo complejo.
1.1 Principios de Redux
Tenemos tres principios básicos que cumplir:
- Single Source Of Truth: Cada pieza de información se almacena en un único lugar, independientemente de dónde sea creada, modificada o requerida.
- Read Only State: La información será de sólo lectura y sólo podrá modificarse a través de conductos oficiales.
- Changes By Pure Functions: Los cambios tienen que poder ser replicados, cancelados y auditados; lo mejor, usar una función pura.
1.2 Elementos de Redux
Los artificios fundamentales que incorporaremos a nuestro desarrollo van en dos niveles. El primer nivel resuelve los dos primeros principios.
- Store: El sistema que mantiene el estado. Despacha acciones de mutado sobre el mismo y comunica cambios enviando copias de sólo lectura a los subscriptores.
- State: Objeto que contiene la única copia válida de la información. Representa el valor del almacén en un momento determinado. Nunca expondremos un puntero a este dato privado.
Acceso al estado
Setters : Métodos que asignan y notifican un nuevo cambio. Clonan la información recibida para que el llamante no tenga un puntero al estado.
Selectors : Métodos para consulta del estado. Devuelven un observable al que suscribirse para obtener notificaciones de cambio o una instantánea. En cualquier caso siempre emitirá o devolverá un clon del estado.
Mutaciones del estado
Actions: Objetos identificados por un tipo y cargados con un payload. Transmiten una intención de mutación sobre el estado del store.
Reducers : Son funciones puras, que ostentan la exclusividad de poder mutar el estado. Reciben dos argumentos: el estado actual y una acción con su tipo y su carga. Clonan el estado, realizan los cambios oportunos y devuelven el estado mutado.
Podemos verlo en un diagrama antes de pasar a verlo en código.
2. Implementación de un Store con RxJs
1 | As a: seller, |
Para que veas que Redux es independiente de Angular, vamos a montarlo en una librería sin dependencias del framework. Sólo TypeScript y RxJs. A día de hoy ambas tecnologías son transversales al ecosistema frontend.
1 | ng g @nrwl/workspace:library rx-store |
2.1 El Store observable mínimo
Empezaremos por la implementación más inocente de un almacén reactivo. Una clase con tipos genéricos que hace uso de la librería de observables RxJs. De esta, toma el BehaviorSubject
que permite notificar cambios y valores iniciales.
Aunque para evitar que otra clase consumidora también emita sin control, sólo exponemos su interfaz asObservable()
. Relacionado con esto también es obligatorio evitar a toda costa punteros al estado. Para ell siempre trabajamos con clones. Los métodos get() set()
usan el spread operator de JS { ...this.state }
tanto al recibir como al devolver el valor del estado.
libs\rx-store\src\lib\mini-store.ts
1 | import { BehaviorSubject, Observable } from 'rxjs'; |
Puedes ver su uso en los test unitarios que le acompañan en libs\rx-store\src\lib\rx-store.spec.ts
.
1 | import { MiniStore } from './mini-store'; |
2.2 El envío de acciones
En una situación más realista y aproximada al patrón Redux no deberíamos asignar el valor de manera directa, pues hay que dejar rastro de cómo se llegó ahí. Necesitamos entonces los conceptos de acciones y función reductora. La idea es que el almacén reciba acciones, invoque al reductor y asigne el valor obtenido. Vamos a verlo.
Empezamos por crear otra clase similar libs\rx-store\src\lib\rx-store.ts
y un par de ayudas. Como mínimo el interface que debe cumplir toda Action
; es muy sencillo y siempre igual: una cadena para especificar el comando y cualquier cosa como argumento de carga. Aprovechando las potencia del TypeScript también creamos un type para asegurar que la reducerFunction
tiene la firma adecuada. La obligamos a recibir como argumentos el estado actual y la acción que se le aplica; teniendo que devolver el nuevo estado.
1 | import { BehaviorSubject, Observable } from 'rxjs'; |
Como puedes ver, los get()
y set()
son ahora privados. El mundo exterior nos envía acciones al método public dispatch(action: Action)
. Ese sería un buen momento para incluir lógica de infraestructura e instrumentación. Hay quien crea un log de lo ocurrido, un histórico de valores… las librerías profesionales nos dan todo eso y mas. Una vez recibida la acción empieza la fiesta de los clones invocando en medio a una función reductora que ahora veremos.
2.3 La función reductora de estado
En el fichero de test libs\rx-store\src\lib\rx-store.spec.ts
tenemos un ejemplo clarificador :
1 | const stockReducer: reducerFunction<ProductStock> = function( |
Definimos stockReducer
como una función pura que cumple con el tipo reducerFunction
. Normalmente estas funciones tienen un switch que evalúa el tipo de la acción. En cada caso se asigna un nuevo estado según la mínima lógica oportuna y al finalizar se devuelve el estado mutado.
Veamos el resto del test para tener una idea de cómo se utiliza.
1 | describe('WHEN: I get an increment ', () => { |
De esta forma ya tenemos un sistema sencillo pero del que podríamos decir que cumple con los principios básicos de Redux.
Resumen
Las aplicaciones reales no son sencillas. Este es un tutorial avanzado que te exige conocimiento previo y dedicación. A cambio espero que te resulte útil y que podáis incorporar las técnicas de detección del cambio y control de estado observable en vuestros proyectos.
Con este tutorial continúas tu formación avanzada en Angular; para poder afrontar retos de tamaño industrial usando te recomiendo que uses El patrón Redux con NgRx en Angular. Todos esos detalles se tratan en el curso avanzado online que imparto con TrainingIT o a medida para tu empresa.
Aprender, programar, disfrutar, repetir.
– Saludos, Alberto Basalo