Fundamentos de los Efectos en NgRx

Side Effects — Llamadas asíncronas, HttpClient y API

Rodrigo Bosarreyes
10 min readJan 23, 2024

¿Qué son los Effects en NgRx?

En NgRx, los “effects” o “side effects” se utilizan para manejar operaciones asíncronas y tareas que implican efectos secundarios en tu aplicación, como llamadas API o interacciones con almacenamiento externo. Los efectos secundarios son importantes porque permiten mantener la lógica asíncrona y los efectos secundarios aislados del manejo de estado principal, el cual se centra en ser predecible y sincrónico.

Los efectos secundarios brindan una manera para que los componentes no interactúen directamente con los servicios, logrando de esta manera aislar los servicios de los componentes.

Las principales características de los side effects son:

  • ✅ Aíslan los efectos secundarios de los componentes, logrando así unos componentes más “puros”.
  • ✅ Son servicios prolongados que se suscriben al observable de cada acción registrada en una Store.
  • ✅ Los efectos filtran las acciones basados en el tipo que les interesa mediante operadores.
  • ✅ Pueden realizar una acción síncrona, asíncrona o retorna una nueva acción.

Instalación de @ngrx/effects

Este paquete es independiente de @ngrx/store que instalamos en los artículos anteriores, por lo que debemos instalarlo. Para ello podemos ejecutar el siguiente comando:

ng add @ngrx/effects@latest

Este comando lo que hará será actualizar nuestro package.json con las dependencias necesarias, las instalará y actualizará el archivo de configuración de la aplicación para añadir la función provideEffects()

¿Cómo se estructuran los Efectos en NgRx?

El proceso de un efecto lo podemos dividir en las siguientes partes:

💉 Servicio de Acciones

En NgRx, el servicio Actions es un servicio especial que puedes inyectar en los efectos. Este servicio proporciona un flujo observable de todas las acciones que se despachan en la aplicación.

Lo interesante de este servicio es que emite las acciones después de que el estado más reciente haya sido reducido. Esto significa que cuando un efecto recibe una acción a través de este servicio, ya se ha actualizado el estado correspondiente a esa acción.

Por ejemplo, si tienes un efecto que necesita realizar una tarea después de que una acción específica se despache y el estado se actualice, puedes utilizar el servicio Actions para “escuchar” esa acción específica.

Imagina que tienes una acción de “cargar usuarios”. Una vez que esta acción se despacha y el estado se actualiza (por ejemplo, indicando que la carga está en progreso), el efecto que escucha esta acción puede comenzar a realizar una petición HTTP para obtener los usuarios. Una vez que obtiene la respuesta, el efecto puede despachar otra acción para actualizar el estado con los datos de los usuarios.

⌗ Metadata

Los efectos son definidos utilizando la función createEffect. Esta función no solo crea un efecto (un flujo observable que reacciona a las acciones de la aplicación), sino que también adjunta información adicional, conocida como metadata, a este flujo.

La metadata es un conjunto de datos que acompaña al efecto y proporciona información adicional sobre cómo debe comportarse el efecto. Por ejemplo, la metadata puede indicar si las acciones emitidas por el efecto deben ser despachadas de vuelta al Store.

Uno de los roles clave de los efectos en NgRx es la posibilidad de despachar nuevas acciones al Store. Si un efecto produce una acción como resultado de su flujo observable, esta acción se envía de vuelta al Store. Por ejemplo, después de realizar una llamada API exitosa, un efecto puede despachar una acción con los datos obtenidos para actualizar el estado en el Store.

Imagina un efecto que escucha acciones de tipo “CARGAR_DATOS”. Cuando esta acción es captada por el efecto, se realiza una llamada API. Una vez que la respuesta es recibida, el efecto emite una nueva acción, como “DATOS_CARGADOS”, con los datos obtenidos. Esta acción emitida por el efecto es entonces despachada automáticamente al Store, gracias a la configuración establecida en la metadata del efecto.

🔀 Operador ofType

En NgRx, el operador ofType es parte de la librería NgRx y se utiliza en combinación con flujos observables.

El propósito principal de ofType es determinar sobre qué acciones específicas un efecto debe actuar. En una aplicación que maneja múltiples tipos de acciones, no todos los efectos necesitan responder a cada acción. ofType permite seleccionar solo aquellas acciones que son relevantes para un efecto particular.

Supongamos que tienes acciones como CARGAR_USUARIOS, ACTUALIZAR_USUARIO y ELIMINAR_USUARIO. Si deseas crear un efecto que solo responda a la acción CARGAR_USUARIOS, usarías ofType de la siguiente manera: actions$.pipe(ofType('CARGAR_USUARIOS')). De esta manera, el efecto solo reaccionará y se ejecutará cuando se despache la acción CARGAR_USUARIOS.

📝 Suscripción de efectos a la Store

Recordemos que los efectos en NgRx son servicios inyectables que permiten manejar operaciones secundarias o side effects, como llamadas a API, navegación, o acciones que no forman parte del flujo sincrónico de actualización del estado. Estas operaciones se realizan en respuesta a acciones despachadas desde el Store.

Los efectos en NgRx se suscriben al observable del Store para escuchar y reaccionar a acciones específicas.

💉 Inyección de Servicios en Efectos

Los efectos en NgRx utilizan servicios inyectados para interactuar con API externas y manejar flujos de datos. Estos servicios encapsulan la lógica de estas interacciones, permitiendo que los efectos se enfoquen en la reacción a acciones y en la orquestación de flujos de datos, manteniendo así una clara separación de responsabilidades y promoviendo la reutilización y mantenimiento del código.

¿Cómo se crean Effects?

Class-Based Effects

Para crear un efecto podemos hacerlo creando una clase @Injectable e inyectar sus dependencias en el constructor (como haríamos en cualquier otra clase), en el constructor debemos inyectar al menos Actions que nos provee del observable de acciones. Vamos a ver un ejemplo:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, exhaustMap, catchError } from 'rxjs/operators';
import { MoviesService } from './movies.service';

@Injectable()
export class MoviesEffects {

loadMovies$ = createEffect(() => this.actions$.pipe(
ofType('[Movies Page] Load Movies'),
exhaustMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError(() => EMPTY)
))
)
);

constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}
  • Usamos el decorador @Injectable() para que la clase pueda inyectarse en otros lugares.
  • Inyectamos el servicio Actions y cualquier otro servicio necesario en el constructor de la clase.
  • Utilizamos el método createEffect del paquete @ngrx/effects para crear un nuevo efecto.
  • Utilizamos el operador ofType del paquete @ngrx/effects para indicar sobre qué acción queremos que se asocie el efecto.
  • Usamos otros operadores RxJS como mergeMap, map, y catchError para manejar el flujo de acciones y efectos secundarios (aquí tienes un artículo donde hablo de estos operadores).

Functional Effects

Para crear efectos funcionales también podemos utilizar la función createEffects, pero en este caso no es necesario crear una clase @Injectable. En este caso, para inyectar las dependencias utilizamos la función inject de angular. Además, es importante destacar el último parámetro de la función createEffects, en este parámetro debemos definir un objeto con, al menos, la variable functional a true, de esta manera se creará un efecto funcional, adicionalmente podemos definir la propiedad dispatch a false para definir que este efecto NO devuelve ningún estado. Vamos a ver un ejemplo:

import { inject } from '@angular/core';
import { catchError, exhaustMap, map, of, tap } from 'rxjs';
import { Actions, createEffect, ofType } from '@ngrx/effects';

import { ActorsService } from './actors.service';
import { ActorsPageActions } from './actors-page.actions';
import { ActorsApiActions } from './actors-api.actions';

export const loadActors = createEffect(
(actions$ = inject(Actions), actorsService = inject(ActorsService)) => {
return actions$.pipe(
ofType(ActorsPageActions.opened),
exhaustMap(() =>
actorsService.getAll().pipe(
map((actors) => ActorsApiActions.actorsLoadedSuccess({ actors })),
catchError((error: { message: string }) =>
of(ActorsApiActions.actorsLoadedFailure({ errorMsg: error.message }))
)
)
)
);
},
{ functional: true }
);

export const displayErrorAlert = createEffect(
() => {
return inject(Actions).pipe(
ofType(ActorsApiActions.actorsLoadedFailure),
tap(({ errorMsg }) => alert(errorMsg))
);
},
{ functional: true, dispatch: false }
);
  • Utilizamos la función createEffect para definir el efecto.
  • Inyectamos las dependencias Actions y ActorsService usando inject.
  • Para filtrar la acción opened del ActorsPageActions utilizamos el operador pipe con ofType.
  • Cuando se recibe una acción de tipo ActorsPageActions.opened, exhaustMap llama a actorsService.getAll(). Si hay una llamada en curso iniciada por una acción previa, las nuevas llamadas serán ignoradas hasta que la actual se complete. Esto previene la sobreposición de múltiples efectos.
  • Dentro del exhaustMap, el servicio actorsService.getAll() se llama para obtener todos los actores. Luego, se utiliza pipe para encadenar los operadores map y catchError.
  • Cuando actorsService.getAll() retorna con éxito, map toma la lista de actores (actors) y los transforma en una acción ActorsApiActions.actorsLoadedSuccess. Esta acción se despachará con la lista de actores como payload.
  • En caso de que actorsService.getAll() falle, catchError captura el error. Utiliza la función of para transformar el mensaje de error en una acción ActorsApiActions.actorsLoadedFailure, que se despachará con el mensaje de error.
  • En el caso de efecto displayErrorAlert, al indicarse que no se despache, al lanzarse esta acción simplemente mostrará un mensaje de error, sin realizar ninguna modificación en el estado.
Alto allí

Este es artículo es parte de mi curso de Introducción a NgRx, donde abordamos temas como Store, Actions, Reducers, Selectors, Effects y Features. Si no te suena algún tema este artículo, ¡te recomiendo revisar mi curso!

Registrar Efecto

Para registrar los efectos, ya sean clases o funcionales, debemos invocar al método provideEffects de @ngrx/effects y como parámetros nuestros efectos:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';

import { AppComponent } from './app.component';
import { MoviesEffects } from './effects/movies.effects';
import * as actorsEffects from './effects/actors.effects';

bootstrapApplication(AppComponent, {
providers: [
provideStore(),
provideEffects(MoviesEffects, actorsEffects),
],
});

Si trabajamos con features, lo registraríamos en nuestro archivo de configuración de rutas invocando al mismo método:

import { Route } from '@angular/router';
import { provideEffects } from '@ngrx/effects';

import { MoviesEffects } from './effects/movies.effects';
import * as actorsEffects from './effects/actors.effects';

export const routes: Route[] = [
{
path: 'movies',
providers: [
provideEffects(MoviesEffects, actorsEffects)
]
}
];

Implementando Efectos en nuestra aplicación

Vamos a recordar nuestra maravillosa aplicación 🎉Ng Party Planner🎉. Ya tenemos una aplicación completamente funcional, incluso si añadimos llamadas API de manera tradicional nos funcionaría perfectamente, pero como hemos visto en este artículo, existe una manera más alineada a la filosofía de NgRx.

La API que vamos a utilizar es https://randomuser.me/, concretamente el endpoint es:

https://randomuser.me/api/?inc=name,email,cell,id,picture&results=10

La implementación en el service sería:

guests.service.ts

Quizás lo más destacable sería la utilización de la clase HttpParams para añadir los parámetros inc y results y el mappeo del objeto que devuelve la API a la clase Guest, además, en algunas ocasiones la API nos devuelve un id vacío y esto lo controlamos con el operador ternario, en caso de no tener un id generamos uno con la librería crypto.randomUUID().

Recuerda instalar la librería @ngrx/effects

Ahora vamos a crear dos nuevas acciones, Page Opened que se lanzará al abrir la página de los Invitados, Guests Loaded Failure que se lanzará al existir un error y Guests Loaded Success que se lanzará cuando se obtengan los datos correctamente (simplemente renombraremos la acción que ya tenemos). Por convención agruparemos estas dos últimas acciones en el nuevo grupo GuestAPIActions (¡recuerda actualizar también el reducer!)

export const GuestActions = createActionGroup({
source: 'Guests',
events: {
'Page Opened': emptyProps(),
// ...demás acciones
},
});

export const GuestAPIActions = createActionGroup({
source: 'Guests API',
events: {
'Guests Loaded Failure': props<{ msg: string }>(),
'Guests Loaded Success': props<{ guests: Array<Guest> }>(),
},
});

Ahora vamos a codificar los efectos, queremos que al entrar a la página de Invitados se carguen los datos mediante la API, este efecto se asociará a la acción Page Opened que invocará a nuestro service y con el resultado del método getGuests invocará a la acción Guests Loaded Success que se encargará de realizar el cambio de estado correspondiente.

guest.effects.ts

También se debe controlar un posible error durante la invocación, en este caso se mostrará un mensaje de error mediante el MessageService y Toast de PrimeNG.

guests.effects.ts

Actualizamos nuestro guest.component.ts para que en el ngOnInit se dispare la acción Page Opened. ¡Mira qué código más limpio!

Por último, registramos nuestros efectos en app.config.ts:

// ...demás dependencias
import * as guestsEffects from './core/store/guests/guest.effects';

export const appConfig: ApplicationConfig = {
providers: [
// ...demás providers
provideEffects(guestsEffects),
],
};

Et voilá!

Si inspeccionamos las acciones disparadas, vemos que cuando accedemos a la página se lanza la acción Page Opened y pasados unos milisegundos se lanza la acción Guests Loaded Success, que es la encargada de ejecutar al reducer con el cambio del estado:

¡Enhorabuena! Hemos aprendido los conceptos fundamentales de esta librería e incluso hemos desarrollado una aplicación totalmente funcional aplicando estos conceptos, pero… ¿Existe alguna manera más “correcta” de hacerlo? La respuesta es ¡Sí!, si implementamos features para agrupar este segmento de estado mejoraríamos la mantenibilidad e incluso el rendimiento de nuestra aplicación. ¡Acompáñame a verlo cómo!

--

--