🇺🇸 EN
Frontend
·7 min read·↗·🇺🇸 EN

Cómo implementar ActionCreationGroup en NgRx

Portada principal del artículo: Cómo implementar ActionCreationGroup en NgRx

Cuando creamos acciones en NgRx, normalmente usamos la función auxiliar createAction. Escribimos los nombres de las acciones con un string en línea que describe el origen y el evento, como [Cart] Update Price. Esta acción queda claramente vinculada con la funcionalidad Cart en nuestra aplicación y con el evento para actualizar el precio. Sin embargo, esta acción no se usa sola: la utilizaremos en varios lugares, como componentes, reducers, servicios o effects. ¿Qué pasa si cambio el nombre o cometo un error tipográfico?

Tendría que actualizarla en cada sitio. Por ejemplo, tenemos la acción '[Home Page] Accept Terms', declarada en home.reducer.ts.

createAction('[Home Page] Accept Terms'), (state) => ({
    ...state,
    acceptTerms: !state.acceptTerms,
  })

Pero homeReducer incluye otras acciones relacionadas con la funcionalidad de home:

export const homeReducer = createReducer(
  initialState,
  on(createAction('[Home Page] Accept Terms'), (state) => ({
    ...state,
    acceptTerms: !state.acceptTerms,
  })),
  on(createAction('[Home Page] Reject Terms'), (state) => ({
    ...state,
    acceptTerms: false,
  })),
);

Estas acciones se disparan desde home.component, por lo que tenemos que repetir el mismo código para despacharlas:

onChange() {
    this._store.dispatch({
      type: '[Home Page] Accept Terms',
    });
  }
  onRejectTerms() {
    this._store.dispatch({
      type: '[Home Page] Reject Terms',
    });
  }

Debemos tener cuidado con la descripción de la acción, ya que es la única forma de que ReduxDevTools muestre información sobre las acciones de REDUX en nuestra aplicación.

¿Por qué tengo que repetir el código en todas partes? ¿Cómo puedo agrupar las acciones relacionadas con una misma funcionalidad? Tengo buenas noticias: la función createActionGroup nos permite crear fácilmente acciones relacionadas con una funcionalidad o sección. Veamos cómo utilizarla.

Acciones en NgRx

Las acciones nos ayudan a describir eventos únicos en nuestra aplicación. Cada acción tiene un string único que representa el origen y el evento que se disparó. Por ejemplo:

export const playersLoad = createAction('[Players API] Players Load')

En otros casos, contamos con un segundo parámetro opcional, props, que nos ayuda a definir un parámetro fuertemente tipado.

export const playersLoadedSuccess = createAction('[Players API] Players Loaded Success', props<{ players: Array<Player>}>)

En lugar de tener acciones separadas, podemos usar la función createActionGroup para crear un grupo de acciones. La función createActionGroup toma un objeto como parámetro, el cual incluye el origen (source) y la lista de eventos (nuestras acciones).

Por ejemplo, agrupemos todas las acciones relacionadas con la página de jugadores. En vez de repetir el nombre de la acción en cada una, importamos createActionGroup, definimos el origen y declaramos los eventos (las acciones). Para acciones sin parámetros, usamos la función emptyProps(), y para acciones con parámetros, usamos la función props.

import { createActionGroup, emptyProps, props } from '@ngrx/store';
 
export const playersLoaded = createActionGroup({
  source: 'Players Page',
    events: {
   		'Players Load': emptyProps(),
	    'Player Loaded Success': props<{ players: Array<any> }>()  
    },
});

Recomiendo leer más sobre Action Groups o tomarte un minuto para ver el excelente video de @Brandon Roberts en https://www.youtube.com/watch?v=rk83ZMqEDV4.

Uso de createActionGroup

Es momento de comenzar a usar createActionGroup en nuestro proyecto. Continuamos con el proyecto inicial de NgRx, lo clonamos y cambiamos a la rama implement-store.

git clone https://github.com/danywalls/start-with-ngrx.git
git switch implement-store

Abre el proyecto con tu editor favorito y crea el archivo src/pages/home/state/home.actions.ts. Importa la función createActionGroup para declarar las acciones 'Accept Terms' y 'Reject Terms'. Agrega dos acciones más para loadPlayers y loadPlayerSuccess, usando las funciones emptyProps y props.

El código final se ve así:

import { createActionGroup, emptyProps, props } from '@ngrx/store';
 
export const HomePageActions = createActionGroup({
  source: 'Home Page',
  events: {
    'Accept Terms': emptyProps(),
    'Reject Terms': emptyProps(),
    'Players Load': emptyProps(),
    'Player Loaded Success': props<{ players: Array<any> }>(),
  },
});

Primero, refactoriza el archivo home.reducer.ts moviendo la interfaz HomeState y el objeto initialState a home.state.ts. Agrega dos nuevas propiedades, loading y players, e inicialízalas en HomeState.

export interface HomeState {
  acceptTerms: boolean;
  loading: boolean;
  players: Array<any>;
}
export const initialState: HomeState = {
  acceptTerms: false,
  loading: false,
  players: [],
};

Luego, crea un nuevo archivo home.actions.ts, importa la función auxiliar createActionGroup, y declara y exporta HomePageActions. Este contendrá todos los eventos (acciones) relacionados con HomePage.

El código final en home.actions.ts queda así:

import { createActionGroup, emptyProps, props } from '@ngrx/store';
 
export const HomePageActions = createActionGroup({
  source: 'Home Page',
  events: {
    'Accept Terms': emptyProps(),
    'Reject Terms': emptyProps(),
    'Players Load': emptyProps(),
    'Player Loaded Success': props<{ players: Array<any> }>(),
  },
});

Ahora es momento de refactorizar home.reducer.ts importando HomePageActions desde home.actions.ts.

import { HomePageActions } from './home.actions';

Elimina las acciones con strings en línea y reemplázalas con HomePageActions de esta forma:

  on(HomePageActions.acceptTerms, (state) => ({
    ...state,
    acceptTerms: !state.acceptTerms,
  })),
  on(HomePageActions.rejectTerms, (state) => ({
    ...state,
    acceptTerms: false,
  })),

Agrega nuevos listeners para las acciones playersLoad y playerLoadedSuccess:

on(HomePageActions.playersLoad, (state) => ({
    ...state,
    loading: true,
  })),
  on(HomePageActions.playerLoadedSuccess, (state, { players }) => ({
    ...state,
    loading: false,
    players,
  })

El código final en home.reducer.ts se ve así:

import { createReducer, on } from '@ngrx/store';
import { initialState } from './home.state';
import { HomePageActions } from './home.actions';
 
export const homeReducer = createReducer(
  initialState,
  on(HomePageActions.acceptTerms, (state) => ({
    ...state,
    acceptTerms: !state.acceptTerms,
  })),
  on(HomePageActions.rejectTerms, (state) => ({
    ...state,
    acceptTerms: false,
  })),
  on(HomePageActions.playersLoad, (state) => ({
    ...state,
    loading: true,
  })),
  on(HomePageActions.playerLoadedSuccess, (state, { players }) => ({
    ...state,
    loading: false,
    players,
  })),
);

Uso de las acciones

Es momento de usar HomePageActions en home.component.ts. En lugar de despachar acciones basadas en strings, primero importa:

import { HomePageActions } from './state/home.actions';

Luego, actualiza this._store.dispatch para usar HomePageActions:

  onChange() {
    this._store.dispatch(HomePageActions.acceptTerms());
  }
  onRejectTerms() {
    this._store.dispatch(HomePageActions.rejectTerms());
  }

Perfecto, pero aún tenemos dos acciones más: loading y players. La idea es mostrar un indicador de carga y cargar una lista de jugadores de prueba.

Usa el store para seleccionar el slice de home y tomar loading y players.

Sí, una mejor opción es usar selectores. Pronto jugaremos con ellos.

public $loading = toSignal(this._store.select((state) => state.home.loading));
public $players = toSignal(
  this._store
    .select((state) => state.home.players)
);

Queremos entrar a home.component y despachar la acción playersLoad en el hook de ciclo de vida ngOnInit. Luego, tras 5 segundos, despachar la acción playerLoadedSuccess con una lista simulada de jugadores.

El código en home.component.ts se ve así:

public ngOnInit(): void {
  this._store.dispatch(HomePageActions.playersLoad());
 
  setTimeout(() => {
    this._store.dispatch(
      HomePageActions.playerLoadedSuccess({
        players: [
          { id: 1, name: 'Lebron', points: 25 },
          { id: 1, name: 'Curry', points: 35 },
        ],
      }),
    );
  }, 5000);
}

Por último, escuchamos las señales $loading e iteramos sobre $players usando @for. El código se ve así:

<div>
@if (!$loading()) {
    <h3>TOP NBA Players</h3>
    @for (player of $players(); track player) {
      <p>{{ player.name }} {{ player.points }}</p>
    }
  } @else {
    <span>Loading..</span>
  }
</div>

Guarda los cambios y ejecuta la aplicación usando ng serve. Abre REDUX DevTools y verás que la acción playersLoad se dispara y aparece el mensaje de carga. Después de 5 segundos, se despacha la acción PlayersLoadedSuccess y se muestra la lista de jugadores.

Conclusión

Aprendimos a compartir acciones en NgRx usando la función createActionGroup para evitar código repetitivo y reducir el riesgo de errores tipográficos. También vimos cómo agrupar acciones relacionadas y cómo refactorizar el código de nuestros reducers y componentes al organizarlas en grupos. Esto hace que mantener nuestras acciones sea mucho más sencillo.

Código fuente: https://github.com/danywalls/start-with-ngrx/tree/action-creators

Parte de la serie Frontend

Modern frontend development — Angular, React, TypeScript, accessibility, and performance.

Ver serie completa

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.