How to Implement ActionCreationGroup in NgRx
Dany Paredes
Posted on July 20, 2024
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,
})
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,
})),
);
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',
});
}
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.
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')
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>}>)
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> }>()
},
});
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
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> }>(),
},
});
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: [],
};
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> }>(),
},
});
It's time to refactor home.reducer.ts
by importing HomePageActions
from home.actions.ts
.
import { HomePageActions } from './home.actions';
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,
})),
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,
})
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,
})),
);
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';
Then, update the this._store.dispatch
to use the HomeActions
.
onChange() {
this._store.dispatch(HomePageActions.acceptTerms());
}
onRejectTerms() {
this._store.dispatch(HomePageActions.rejectTerms());
}
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)
);
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);
}
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>
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.
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
Posted on July 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.