How to Implement ActionCreationGroup in NgRx

danywalls

Dany Paredes

Posted on July 20, 2024

How to Implement ActionCreationGroup in NgRx

When you create actions in NgRx, you usually use the createAction function helper. We write our action names with an inline string about the source and the event, like [Cart] Update Price. This action is clearly linked with the Cart feature in our application and the event to update the price. However, this action is not used alone; it will be used in several places, like the component, reducer, service, or effects. What happens if I change the name or have a typo?

I must update it in every place. For example, we have the action '[Home Page] Accept Terms', which is declared in the home.reducer.ts.



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


Enter fullscreen mode Exit fullscreen mode

But the homeReducer, include other actions related to the home feature:



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,
  })),
);


Enter fullscreen mode Exit fullscreen mode

Those actions are triggered by the home.component, so we need to repeat the same code to dispatch the actions:



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


Enter fullscreen mode Exit fullscreen mode

We need to be careful with the action description because it is the only way to make the ReduxDevTools provide information about the REDUX actions in our application.

1

Why do I need to repeat the code everywhere? How do I group my actions related to a feature? I have good news for you: the createActionGroup function allows us to easily create actions related to a feature or section. Let's explore how to use it!

Actions In NgRx

The actions help us describe unique events in our application. Each action has a unique string that represents the source and the event that was triggered. For example:



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


Enter fullscreen mode Exit fullscreen mode

In other cases, we have a second optional parameter, props, which helps by providing a strongly typed parameter.



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


Enter fullscreen mode Exit fullscreen mode

Instead of having separate actions, we can use the createActionGroup function to create a group of actions. The createActionGroup function takes an object as a parameter, which includes the source and the list of events (our actions).

For example, let's group all actions related to the players' page. Instead of repeating the action name for each one, we import createActionGroup, set the source, and declare the events (the actions). For actions without parameters, use the emptyProps() function, and for actions with parameters, use the props function.



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> }>()  
    },
});


Enter fullscreen mode Exit fullscreen mode

I highly recommend reading more about Action Groups or taking a minute to watch the amazing @Brandon Roberts video https://www.youtube.com/watch?v=rk83ZMqEDV4.

Using createActionGroup

It's time to start using createActionGroup in our project. We continue with the initial project of NgRx, clone it, and switch to the implement-store branch.



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


Enter fullscreen mode Exit fullscreen mode

Open the project with your favorite editor, and create a src/pages/home/state/home.actions.ts file. Import the createActionGroup function to declare the 'Accept Terms' and 'Reject Terms' actions. Add two more actions for loadPlayers and loadPlayerSuccess, using the emptyProps and props functions.

The final code looks like this:



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> }>(),
  },
});


Enter fullscreen mode Exit fullscreen mode

First, refactor the home.reducer.ts file by moving the HomeState interface and initialState to home.state.ts. Add two new properties, loading and players, and initialize them in the HomeState.



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


Enter fullscreen mode Exit fullscreen mode

Next, create a new file home.actions.ts, import the createActionGroup function helper, and declare and export HomePageActions. This will contain all events (actions) related to the HomePage.

The final code in home.actions.ts looks like this:



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> }>(),
  },
});


Enter fullscreen mode Exit fullscreen mode

It's time to refactor home.reducer.ts by importing HomePageActions from home.actions.ts.



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


Enter fullscreen mode Exit fullscreen mode

Remove the inline string actions and replace them with HomePageActions like this:



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


Enter fullscreen mode Exit fullscreen mode

Add new listeners for the playersLoad and playerLoadSuccess actions:



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


Enter fullscreen mode Exit fullscreen mode

The final code in home.reducer.ts looks like:



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,
  })),
);


Enter fullscreen mode Exit fullscreen mode

Using Actions

It's time to use HomeActions in the home.component.ts. Instead of dispatching string actions, first import:



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


Enter fullscreen mode Exit fullscreen mode

Then, update the this._store.dispatch to use the HomeActions.



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


Enter fullscreen mode Exit fullscreen mode

Perfect, but we have two more actions, loading and players. The idea is to show a loading indicator and load a list of fake players.

Use the store to select the home slice and pick the loading and players.

Yes, a better way is using selectors. We are going to play with them soon.



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


Enter fullscreen mode Exit fullscreen mode

We want to enter home.component and dispatch the playersLoad action in the ngOnInit lifecycle hook. Then, after 5 seconds, dispatch the playerLoadSuccess action with a list of fake players.

The code in home.component.ts looks like this:



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);
}


Enter fullscreen mode Exit fullscreen mode

Finally, we listen for the $loading signals and iterate over the $players using @for. The code looks like this:



<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>


Enter fullscreen mode Exit fullscreen mode

Save your changes and run the app using ng serve. Open the REDUX DevTools, and you will see the playersLoad action is triggered, and the loading message appears. After 5 seconds, the PlayersLoadedSuccess action is dispatched, and the list of players is shown.

2

Conclusion

We learned how to share NgRx actions using the createActionGroup function to avoid repetitive code and reduce the risk of typos. We also learned how to group related actions and refactor our reducer and component code by grouping actions. This makes it easier to maintain our actions.

Source Code: https://github.com/danywalls/start-with-ngrx/tree/action-creators

💖 💪 🙅 🚩
danywalls
Dany Paredes

Posted on July 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related