En el artículo anterior, explique cómo llegué a usar NgRx. A continuación, compartiré las mejores prácticas en  una aplicación de ejemplo "Eternal". Aquí, veremos la manera en que la gestión de estados te permite añadir funcionalidad de caché a tu código.

Recuerda que si tu quieres leer más contenido acerca de esté tema, puedes visitar mi blog.

Parte 1: Cache y LoadStatus

Este patrón asegura que el store no cargue datos que ya tiene. En otras palabras: Añade una funcionalidad de caché.

Creamos este patrón en dos pasos. El estado obtiene una propiedad adicional llamada loadStatus, que emplea internamente para determinar si se requiere una petición a un endpoint.

Los ejemplos de State management suelen usar una acción
loady otra loaded para implementar una petición a un endpoint.

Nuestro patrón añade una tercera acción llamada get. Los componentes solo deben utilizar la acción get y es solo para uso interno en la gestión de estados.

El diagrama de abajo muestra a grandes rasgos en qué orden las acciones, los efectos y los reducers trabajan juntos para cargar los datos contra un estado vacío.

Si el estado ya tiene datos, los componentes pueden lanzar la acción get tantas veces como quieran, ya que no dará lugar a peticiones innecesarias:

Demostración

En nuestro ejemplo, hay un componente que lista a los clientes y otro componente que muestra un formulario detallado.

Ambos componentes necesitan lanzar el método load, estos necesitan los datos de los clientes y tienen que asegurarse de que se cargan.

Se podría argumentar que los usuarios siempre siguen el camino de la vista general a la vista detallada. Por lo tanto, debería ser suficiente que sólo la vista de lista despache la acción.

No podemos confiar únicamente en eso. Los usuarios pueden tener un enlace profundo directamente al formulario. Tal vez algunos otros componentes de la aplicación se vinculen directamente allí también.

Ahora tenemos el problema de que "hacer clic a través de la lista de usuarios" terminará creando un montón de llamadas innecesarias al endpoint.

Para solucionarlo, introducimos una propiedad loadStatus.

Los datos del store pueden estar en tres estados diferentes:

  • Pueden no estar cargados.
  • Se pueden cargar.
  • o están cargados.

Además, sólo queremos renderizar nuestros componentes, cuando los datos están presentes.

El LoadStatus es un tipo de unión con tres valores diferentes. El estado lo tiene como propiedad y su valor inicial es "NOT_LOADED".

El estado cambia de

export interface State {

customers: Customer[];

}

const initialState = {

customers: []

}

A lo siguiente

export interface State {

loadStatus: 'NOT_LOADED' | 'LOADING' | 'LOADED';

customers: Customer[];

}

const initialState = {

loadStatus: 'NOT_LOADED',

customers: []

}

Introducimos una acción más, que llamamos get. Los componentes sólo utilizarán esa acción. A diferencia del método load, el get notifica al store que se solicitan los datos.

Un effect maneja ese método get. Comprueba el estado actual y, si el estado no es "LOADED", envía la acción load original. Ten en cuenta que la acción load es ahora una acción "interna". Los componentes o servicios nunca deben lanzarla.

Junto al effect que se ocupa de la acción de load, también tenemos un reducer adicional. Este establece el loadStatus a "LOADING". Esto tiene el el beneficio de que no pueden ocurrir peticiones paralelas. Eso está asegurado por diseño.

Lo último que tenemos que hacer es modificar nuestros selectores. Sólo deben emitir los datos si loadStatus se establece como LOADED. En consecuencia, nuestros componentes sólo pueden renderizar si los datos están totalmente disponibles.

Otras consideraciones

¿Por qué no podemos tomar un valor nulo en lugar del loadStatus como indicador de que el estado no se ha cargado todavía? Como consumidores del state, es posible que no conozcamos el valor inicial, por lo que sólo podemos adivinar si es nulo o no. Null puede ser en realidad el valor inicial que recibimos del backend. O puede ser algún otro valor.  Teniendo un valor explícito de loadStatus, podemos estar seguros.

Lo mismo ocurre si se trata de un array. ¿Un array vacío significa que el almacén acaba de ser inicializado o significa que realmente no tenemos ningún dato? No queremos mostrar al usuario "Lo sentimos, no se han encontrado datos" cuando en realidad la petición espera la respuesta.

Casos avanzados

Con interfaces complejas, el store puede fácilmente recibir múltiples acciones en un periodo de tiempo muy corto. Cuando diferentes componentes disparan la acción de load, por ejemplo, todas estas acciones juntas construyen el estado que algún otro componente quiere mostrar.

Un caso de uso similar podría ser acciones encadenadas. Una vez más, un componente dependiente sólo quiere renderizar cuando la última acción ha terminado.

Sin la propiedad LoadStatus, el selector del componente emitiría cada vez que el estado cambie parcialmente. Esto puede resultar en un efecto de parpadeo poco amigable para el usuario.

En su lugar, los selectores deberían comprobar primero el LoadStatus antes de devolver los datos reales. Esto tiene la ventaja de que el componente obtiene los datos sólo una vez y en el momento adecuado, esto es muy eficiente y eficaz.

Extensiones

Si tenemos varios componentes que requieren los mismos datos y los componentes son todos hijos de la misma ruta, podemos utilizar un Guard para enviar la acción de obtención y esperar los datos.

En nuestro caso, tanto la lista como el detalle son hijos de "cliente". Así que nuestra guard se ve así:

@Injectable({

providedIn: 'root',

})

export class DataGuard implements CanActivate {

constructor(private store: Store<CustomerAppState>) {}

canActivate(): Observable<boolean> {

this.store.dispatch(CustomerActions.get());

return this.store

.select(fromCustomer.isLoaded)

.pipe(filter((isLoaded) => isLoaded));

}

}

Si realmente se busca la perfección, se podría incluso extraer el envío a un componente que se sitúe al lado del guard. La razón es que los guard deben ser pasivos y no tienen efectos secundarios.

Mejores prácticas relacionadas

En artículos posteriores, veremos las mejores prácticas relacionadas con nuestro ejemplo de almacenamiento en caché. Puede que también tengas algún contexto para esos datos, como un paginado asíncrono o una búsqueda.

Sea cual sea el contexto, la cuestión es que el frontend posee un subconjunto de datos que depende de ciertos "parámetros de filtrado" como la página actual. Si estos cambian, tenemos que encontrar una manera de invalidar la caché. Por favor, puede investigar más sobre esto.

En otro caso, podemos querer evitar que un consumidor active manualmente la acción de carga de datos con la llamada al endpoint. No podemos hacerlo a menos que encapsulemos la acción en un módulo propio y proporcionemos una interfaz para ella: Facade.

Perspectiva a futuro

El próximo artículo se centra en la arquitectura. Descubriremos cómo estructurar nuestra aplicación para que la gestión del estado pueda ser añadida como un módulo y cómo los componentes deben acceder a ello.

Traducción en español del artículo original de Rainer Hahnekamp NgRx Best Practices Series: 1. Cache & LoadStatus publicado el 23 agosto 2021
Plataforma de cursos gratis sobre programación