Fundamentos de las Acciones en NgRx

Dominando Actions — Creación, Tipos y Payloads

Rodrigo Bosarreyes
7 min readJan 23, 2024

¿Qué son las Actions en NgRx?

Las acciones nos ayudan a entender los eventos que ocurren en la aplicación y desencadenan un cambio de estado.

Las acciones son el único medio para comunicar intenciones de cambiar el estado. Cuando algo sucede en la aplicación, como un usuario haciendo clic en un botón de Añadir, se despacha una acción. Esta acción es manejada por funciones llamadas reducers, que son responsables de escuchar estas acciones y devolver un nuevo estado basado en la acción recibida.

Un detalle importante es que si hay efectos registrados que escuchen este tipo de acción, ellos también se activarán. Los efectos generalmente se utilizan para manejar operaciones asíncronas como llamadas a APIs, y pueden despachar nuevas acciones basadas en los resultados de estas operaciones (lo veremos más adelante).

¿Cómo se crean Actions?

Antes implementar acciones, vamos a ver un poco de teoría.

Debemos considerar las recomendaciones/buenas prácticas del equipo de NgRx para escribir acciones. Estas son:

  • ✅ Escribe acciones antes de desarrollar: Esto te ayuda a planificar mejor el flujo de datos y las interacciones en la aplicación. Por ejemplo, si estamos desarrollando un e-commerce tendríamos las acciones (o los eventos) Productos Cargados (ProductsLoaded), Producto Añadido Al Carrito (ProductAddedToCart), Producto Eliminado Del Carrito (ProductRemovedFromCart).
  • ✅Categoriza las acciones según eventos: Puedes agruparlas en categorías basadas en los eventos del dominio de la aplicación. Por ejemplo, siguiendo con el ejemplo anterior, podrías tener las categorías de Products y Cart.
  • ✅Entre más acciones, mayor comprensión: Usa acciones específicas para describir eventos únicos en lugar de acciones genéricas. Esto mejora la legibilidad y el mantenimiento del código, aunque aumenta su complejidad. En lugar de tener solamente una acción CartChanged es mejor tener las acciones ProductAddedToCart y ProductRevomedFromCart.
  • ✅Captura eventos, no órdenes: Las acciones deben representar eventos que han sucedido, no órdenes o instrucciones para hacer algo. Por ejemplo, en lugar de la acción LoadProducts o AddProductToCart, que suena a una orden, es mejor usar ProductsLoaded o ProductAddedToCart.
  • ✅Provee un contexto útil y único al evento: Las acciones deben ser descriptivas y destinadas a un único evento en un contexto claro y específico. Por ejemplo, si la acción ProductsLoaded es producida después de realizar una llamada a alguna API, en esta acción estaríamos contemplando dos eventos, cuando se carga correctamente y cuando algo falla, por lo que sería recomendable dividirla en dos: ProductsLoadSuccess y ProductsLoadFailure.

Ahora sí, teniendo en cuenta las buenas prácticas, podemos ver cómo se implementarían las acciones.

La implementación de un Action es simplemente una extensión de la interfaz con el mismo nombre:

interface Action {
type: string;
}

Esta interfaz tiene solo una propiedad, el tipo, que es un string. El tipo se utiliza para describir la acción que será atendida. El valor de esta propiedad debe seguir la nomenclatura [Fuente] Evento. Además, en caso de que necesites añadir información adicional para que la acción sea atendida correctamente, puedes añadir nuevas propiedades.

Antiguamente, la única manera de crear acción era mediante clases, afortunadamente a partir de la versión 8 se implementó una manera más cómoda, las “Functional Actions” que son las que utilizaremos. Dentro de estas functional actions existen dos funciones que podemos utilizar.

createAction

Esta función retorna otra función (🤨) que cuando es llamada retorna un objeto (🥴) de la interfaz Action de ngrx. La función createAction puede recibir dos parámetros: tipo de la acción y un objeto metadata (payload) para información adicional que se necesite al recibir la acción.

👍

En otras palabras, createAction nos genera un Action con el tipo y con el payload (propiedades) que le indiquemos. As simple as that. Veamos un ejemplo:

Queremos crear la acción ProductAddedToCart, para ello creamos el archivo cart.actions.ts y utilizamos la función createAction:

import { createAction, props } from '@ngrx/store';

export const productAddedToCart = createAction(
'[Cart] Product Added',
props<{ id: string; name: string }>()
);

createActionGroup

Esta función crea un grupo de acciones que pertenecen a la misma fuente/origen. Es prácticamente lo mismo que createAction, pero orientado a un contexto concreto.

Esta función recibe como parámetro un objeto con las propiedades “source” que es un string con el nombre del contexto y “events” que es un map donde la clave es el nombre del evento (acción) y el valor el payload (metadata) de la acción. Vamos a verlo en un ejemplo:

import { createActionGroup, props, emptyProps } from '@ngrx/store';

export const CartActions = createActionGroup({
source: 'Cart',
events: {
'Product Added': props<{ id: string; name: string }>(),
'Product Removed': props<{ id: string }>(),
'Cleaned': emptyProps(),
},
});

¿Cómo se utilizan las Actions?

Las acciones se pueden utilizar (o “despachar”) desde los componentes o servicios. Para lanzar este proceso debemos invocar el método dispatch del servicio Store provisto por NgRx.

El método dispatch toma un objeto de acción como su argumento y lo envía al Store. Recordemos que las acciones son objetos que tienen al menos una propiedad type, que indica el tipo de acción que se está llevando a cabo, y opcionalmente pueden tener un payload, que contiene los datos o información relacionada con la acción.

Veamos un par de ejemplo:

  • Si hemos utilizado la función createAction:
import { productAddedToCart } from './actions/cart.actions';

// ... en algún componente o servicio
this.store.dispatch(productAddedToCart({ id: '1-2-3-4', name: 'Peluche Peepo' }));
  • Si hemos utiliza la función createActionGroup:
import { CartActions } from './actions/cart.actions';

// ... en algún componente o servicio
this.store.dispatch(CartActions.productAdded({ id: '1-2-3-4', name: 'Peluche Peepo' }));
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!

Implementando acciones en nuestra aplicación

Vamos a recordar nuestra maravillosa aplicación 🎉Ng Party Planner🎉, en el artículo anterior implementamos toda una store, con sus reducers, selectors y actions, pero solo implementamos una y si te das cuenta, esa una no termina de cumplir con las buenas prácticas comentadas, así que vamos a corregirlo.

¡Recuerda que puedes acceder al repositorio para ver el código cambiado en este módulo!

En primer lugar, vamos a ver cuáles son las acciones de esta aplicación:

Caso de Uso NgPartyPlanner
  • Guest Loaded: Esta acción corresponde a la carga del listado de invitados, además, este listado será el payload.
  • Guest Added: Esta acción corresponde a cuando se añade un nuevo invitado. Este nuevo invitado será el payload.
  • Guest Removed: Esta acción corresponde a cuando se elimina un invitado. Solamente el id será el payload.
  • Attendence Confirmed: Esta acción corresponde a la confirmación de la asistencia de un invitado. El payload será únicamente el id.
  • Attendence Declined: Esta acción corresponde al rechazo de la asistencia de un invitado. El payload será únicamente el id.

Vamos a picar un poco, abre el archivo guest.actions.ts y refactoriza el código para reemplazar la función createAction por createActionGroup, además, cambia el nombre de la variable por GuestActions. El source sería Guests y de momento solo vamos a crear la primera acción:

import { createActionGroup, props } from '@ngrx/store';
import { Guest } from './guest.model';

export const GuestsActions = createActionGroup({
source: 'Guests',
events: {
'Guests Loaded': props<{ guests: Array<Guest> }>(),
},
});

Como hemos cambiado el nombre de la variable y la variable en sí, nos vemos obligados a cambiar también el reductor, para ello simplemente debemos cambiar el import y la utilización de la variable:

import { createReducer, on } from '@ngrx/store';
import { GuestsActions } from './guest.actions';
import { Guest } from './guest.model';

const initialState: Array<Guest> = [];

export const guestsReducer = createReducer(
initialState,
on(
GuestsActions.guestsLoaded,
(_state, { guests }): Array<Guest> => [...guests]
)
);

Y por supuesto, hacer el mismo cambio en nuestro componente guests-dataview.component.ts:

...
import { GuestActions } from '../../../../core/store/guests/guest.actions';

...
ngOnInit(): void {
this.store.dispatch(
GuestActions.guestsLoaded({
...

Por último, simplemente sería añadir las demás acciones:

guest.actions.ts

En TypeScript, Pick es un tipo genérico utilitario que permite crear un nuevo tipo seleccionando un conjunto de propiedades de un tipo existente. En este ejemplo Pick<Guest, 'id'>, Pick se utiliza para crear un nuevo tipo que contiene únicamente la propiedad id del tipo Guest. Más info: https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys

E implementamos las invocaciones en el componente. En el caso de confirmar/rechazar asistencia es bastante sencillo:

guests-dataview.component.ts

El caso de Añadir un nuevo Invitado puede parecer más complicado, pero la lógica es la misma, es decir, una vez generado el nuevo invitado notificamos al Store para que lo añada (línea 4):

guests-dataview.component.ts

Y por último, el caso de eliminar invitado es más de lo mismo, con la diferencia que aquí, al utilizar el componente ConfirmationDialog de PrimeNG, nos vemos obligados a lanzar la acción cuando se pulsa el botón de Aceptar de dicho componente:

guests-dataview.component.ts

Hemos definido las acciones o eventos de nuestra aplicación e incluso las invocamos, pero si hacemos clic sobre alguno de los botones vemos que no pasa nada. Hemos dicho que estas acciones son manejadas por “reducers” que responden a estas acciones, pero ¿qué son los reducers?, ¿cómo interactúan las acciones y los reducers? ¡Aquí es donde las piezas comienzan a encajar!

--

--