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 tipologin
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 operadortap
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
allCoursesLoaded
y se pasan los cursos cargados como elpayload
a la acción. - Y finalmente, la acción
allCoursesLoaded
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) );