Elementos fundamentales de NgRx: Store, Actions, Reducers, Selectors, Effects

El store es el elemento clave en todo el proceso de gestión del estado. Mantiene el estado y facilita la interacción entre los componentes y el estado. Puedes obtener una referencia al almacén a través de la inyección de dependencias de Angular, como se muestra a continuación.

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

Esta referencia al almacén puede utilizarse posteriormente para dos operaciones principales:

  • Para enviar acciones al almacén a través del método store.dispatch(…), que a su vez desencadenará reductores y efectos
  • Para recuperar el estado de la aplicación a través de selectores

Estructura de un árbol de objetos de estado

Supongamos que su aplicación consta de dos módulos de funciones llamados Usuario y Producto. Cada uno de estos módulos maneja diferentes partes del estado general. La información del producto siempre se mantendrá en la sección products del estado. La información del usuario siempre se mantendrá en la sección user del estado. Estas secciones también se denominan slices.

Acciones

Una acción es una instrucción que se envía al almacén, opcionalmente con algunos metadatos (payload). Basándose en el tipo de acción, el almacén decide qué operaciones ejecutar. En el código, una acción está representada por un objeto JavaScript simple con dos atributos principales, a saber, type y payload. payload es un atributo opcional que será utilizado por los reductores para modificar el estado. El siguiente fragmento de código y la figura ilustran este concepto.

{ "type": "Login Action", "payload": { userProfile: user }}

La versión 8 de NgRx proporciona una función de utilidad llamada createAction para definir creadores de acciones (no acciones, sino creadores de acciones). A continuación se muestra un código de ejemplo para esto.

A continuación, puede utilizar el creador de acciones login (que es una función) para construir acciones y enviar a ellos a la tienda como se muestra a continuación. user es el objeto payload que se pasa a la acción.

this.store.dispatch(login({user}));

Reductores

Los reductores se encargan de modificar el estado y devolver un nuevo objeto de estado con las modificaciones. Los reductores toman dos parámetros, el estado actual y la acción. Basándose en el tipo de acción recibida, los reductores realizarán ciertas modificaciones al estado actual y producirán un nuevo estado. Este concepto se muestra en el siguiente diagrama.

De forma similar a las acciones, NgRx proporciona una función de utilidad llamada createReducer para crear reductores. Una típica llamada a la función createReducer sería como la siguiente.

Como puedes ver, toma el estado inicial (el estado al inicio de la aplicación) y funciones de cambio de estado de uno a varios que definen cómo reaccionar a diferentes acciones. Cada una de estas funciones de cambio de estado recibe el estado actual y la acción como parámetros, y devuelve un nuevo estado.

Efectos

Los efectos permiten realizar efectos secundarios cuando se envía una acción al almacén. Vamos a tratar de entender esto a través de un ejemplo. Cuando un usuario se registra con éxito en una aplicación, una acción con type Login Action será enviada al almacén con la información del usuario en el payload. Una función reductora escuchará esta acción y modificará el estado con la información del usuario. Además, como efecto secundario, también se quiere guardar la información del usuario en el almacenamiento local del navegador. Un efecto puede ser utilizado para llevar a cabo esta tarea adicional (efecto secundario).

Hay múltiples maneras de crear efectos en NgRx. A continuación se presenta una forma cruda y autoexplicativa de crear efectos. Tenga en cuenta que generalmente no se utiliza este método para crear efectos. Sólo tomé esto como un ejemplo para explicar lo que sucede detrás de la cortina.

  • actions$ observable emitirá acciones recibidas por la tienda. Estos valores pasarán por una cadena de operadores.
  • ofType es el primer operador utilizado. Este es un operador especial proporcionado por NgRx (No RxJS) para filtrar las acciones basadas en su tipo. En este caso, sólo las acciones de tipo login podrán pasar por el resto de la cadena de operadores.
  • tap es el segundo operador utilizado en la cadena para almacenar la información del usuario en el almacenamiento local del navegador. El operador tap se utiliza generalmente para realizar efectos secundarios en una cadena de operadores.
  • Por último, tenemos que suscribirnos manualmente al observable login$.

Sin embargo, este enfoque tiene un par de inconvenientes importantes.

  • Tienes que suscribirte manualmente al observable, lo que no es una buena práctica. De esta manera, siempre tendrás que desuscribirte manualmente, lo que conduce a la falta de mantenibilidad.
  • Si aparece un error en la cadena de operadores, el observable se equivocará y dejará de emitir valores posteriores (acciones). Como resultado, el efecto secundario no se realizará. Por lo tanto, usted tiene que tener un mecanismo en el lugar para crear manualmente una nueva instancia observable y volver a suscribir si se produce un error.

Con el fin de superar estos problemas, NgRx proporciona una función de utilidad llamada createEffect para crear efectos. Una llamada típica a la función createEffect tendría el siguiente aspecto.

El método createEffect recibe una función que devuelve un observable y (opcionalmente) un objeto de configuración como parámetros.

NgRx se encarga de la suscripción al observable devuelto por la función de soporte, y por lo tanto no hay que suscribirse o desuscribirse manualmente. Además, si se produce algún error en la cadena del operador, NgRx creará un nuevo observable y volverá a suscribirse para garantizar que el efecto secundario siempre se ejecute.

Si dispatch es true (valor por defecto) en el objeto de configuración, el método createEffect devuelve un Observable<Action>. En caso contrario, devuelve un Observable<Unknown>. Si la propiedad dispatch es true, NgRx se suscribirá al observable devuelto de type Observable<Action> y enviará las acciones recibidas al almacén.

Si no está mapeando la acción recibida a un tipo de acción diferente en la cadena de operadores, tendrá que establecer dispatch a false. De lo contrario, la ejecución resultará en un bucle infinito, ya que la misma acción será despachada y recibida en el flujo actions$ una y otra vez. Por ejemplo, usted no tiene que establecer dispatch a false en el código de abajo porque se asigna la acción original a un tipo diferente de acción en la cadena de operador.

En el escenario anterior,

  • Efecto recibe acciones de type loadAllCourses.
  • Se invoca una API y se cargan cursos como efecto secundario.
  • Se mapea la respuesta de la API a una acción de type allCoursesLoadedy se pasan los cursos cargados como el payload a la acción.
  • Y finalmente, la acciónallCoursesLoaded que se ha creado se envía al almacén. Esto lo hace NgRx bajo el capó.
  • Un reductor escuchará la acción allCoursesLoaded entrante y modificará el estado con los cursos cargados.

Selectores

Los selectores son funciones puras que se utilizan para obtener trozos del estado del almacén. Como se muestra a continuación, se puede consultar el estado incluso sin usar selectores. Pero este enfoque viene de nuevo con un par de contras importantes.

const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
  • store es un observable al que te puedes suscribir. Cada vez que el almacén recibe una acción, store empujará el objeto de estado a sus suscriptores.
  • Puedes utilizar funciones de mapeo para obtener rebanadas de estado y llevar a cabo cualquier cálculo si es necesario. En el ejemplo anterior, estamos obteniendo el slice user en el árbol de objetos de estado y convirtiéndolo en un booleano para determinar si el usuario ha iniciado sesión o no.
  • Puedes suscribirte manualmente al observable isLoggedIn$ o utilizarlo en una plantilla de Angular con async pipe para leer los valores emitidos.

Sin embargo, este enfoque tiene un inconveniente importante. En general, el almacén recibe acciones con frecuencia desde diferentes partes de la aplicación. Según la implementación anterior, cada vez que el almacén recibe una acción, un objeto de estado será emitido por el almacén. Y este objeto de estado pasará de nuevo por la función de mapeo y actualizará la UI.

Sin embargo, si el resultado de la función de mapeo no ha cambiado desde la última vez, no hay necesidad de actualizar la UI de nuevo. Por ejemplo, si el resultado de la map(state => !!state.user) no ha cambiado desde la última ejecución, no tenemos que volver a empujar el resultado a la UI/Suscriptor. Para lograr esto, NgRx (no RxJS) ha introducido un operador especial llamado select. Con el operador select, el código anterior cambiará de la siguiente manera.

const isLoggedIn$ = this.store.pipe(select(state => !!state.user));

El operador select evitará que los valores sean empujados a la UI/suscriptores si el resultado de la función de mapeo no ha cambiado desde la última vez.

Este enfoque puede ser mejorado aún más. Incluso si el operador select no empuja los valores sin cambios a UI / suscriptores, todavía tiene que tomar el objeto de estado y hacer el cálculo para derivar el resultado cada vez.

Como ya se ha explicado anteriormente, un state será emitido por el observable cuando el almacén recibe una acción de la aplicación. Una acción no siempre actualiza el estado. Si el estado no ha cambiado, el resultado del cálculo de la función de mapeo tampoco cambiará. Por lo tanto, no tenemos que hacer el cálculo de nuevo si el objeto state emitido no ha cambiado desde la última vez. Aquí es donde entran en juego los selectores.

Un selector es una función pura que mantiene una memoria de las ejecuciones anteriores. Mientras la entrada no haya cambiado, la salida no será recalculada. En su lugar, la salida será devuelta desde la memoria. Este proceso se llama memoización.

NgRx proporciona una función de utilidad llamada createSelector para construir selectores con capacidad de memoización. A continuación se muestra un ejemplo de la función de utilidad createSelector.

La función createSelector toma funciones de mapeo de uno a muchos que dan diferentes rebanadas del estado y una función de proyector que lleva a cabo el cálculo. La función del proyector no será invocada si las rebanadas de estado no han cambiado desde la última ejecución. Para utilizar la función selectora creada, hay que pasarla como argumento al operador select.

this.isLoggedIn$ = this.store .pipe( select(isLoggedIn) );

Deja una respuesta

Tu dirección de correo electrónico no será publicada.