Mixing Action Styles in NgRx State
May 14, 2020 - 6 min readPrior to version 8 of the NgRx platform, actions were created using enums, classes, and union types. Many people thought this approach was too noisy, and refer to it as boilerplate 😉. In version 8, we introduced the new creator functions for actions, reducers, and effects. Recently, the question was asked, if you have an existing application, can you use the old syntax with the new syntax? Let's mix things up.
For this example, I'll start with the counter example using StackBlitz from the NgRx docs.
Using with Reducers
To separate the two action styles, put them in different files. In the counter.actions.ts
file, there is an action creator.
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter Component] Increment');
Create a new file named legacy-counter.actions.ts
. In the actions file, define an Increment action using the action class syntax.
import { Action } from '@ngrx/store';
export enum CounterActionTypes {
Increment = '[Counter Component] Legacy Increment',
}
export class Increment {
readonly type = CounterActionTypes.Increment;
}
export type Union = Increment;
The action type is different than the modern action using the creator function. In the counter.reducer.ts file, import the legacy actions. Before mixing the types of the legacy and modern syntax together, you need to create a union type of the two. The @ngrx/store
package contains a helper utility function named union
for returning the types of a dictionary of creator functions.
- Import the actions from
counter.actions.ts
using module import syntax - Pass the object to the union function using the spread operator
import * as CounterActions from './counter.actions';
...
const CounterActionsUnion = union({...CounterActions});
This returns you the return types of the action creators. You already have an existing union of legacy counter actions, so you create a superset of the unions.
type Actions = LegacyCounterActions.Union | typeof CounterActionsUnion;
The reducer creation function handles action creators, but you still need a way to handle action classes. Use a simple switch case to handle this scenario. The switch case handles your legacy actions, and the default uses the created reducer function.
import { createReducer, on, union } from '@ngrx/store';
import * as LegacyCounterActions from './legacy-counter.actions';
import * as CounterActions from './counter.actions';
export const initialState = 0;
type State = number;
const counterReducer = createReducer(
initialState,
on(CounterActions.increment, (state) => state + 1)
);
const CounterActionsUnion = union({ ...CounterActions });
type Actions = LegacyCounterActions.Union | typeof CounterActionsUnion;
export function reducer(state: State | undefined, action: Actions) {
switch (action.type) {
case LegacyCounterActions.CounterActionTypes.Increment:
return state + 1;
default:
return counterReducer(state, action);
}
}
The reducer handles both actions with the same type safety as before. To read more about the redesign of actions in NgRx, visit NgRx: Action Creators redesigned by NgRx team member Alex Okrushko.
Dispatching Actions
Dispatching actions hasn't changed. For an action class, dispatch the action using the created instance. For an action creator, dispatch the action using the called function.
export class MyCounterComponent {
...
increment() {
this.store.dispatch(CounterActions.increment());
}
legacyIncrement() {
this.store.dispatch(new LegacyCounterActions.Increment());
}
...
}
Using with Effects
Using the two different action styles with Effects is more straightforward. Let's look a counter effects example.
import * as LegacyCounterActions from './legacy-counter.actions';
import * as CounterActions from './counter.actions';
@Injectable()
export class CounterEffects {
increment$ = createEffect(
() => {
return this.actions$.pipe(
ofType(
LegacyCounterActions.CounterActionTypes.Increment,
CounterActions.increment
),
tap((count) => console.log('incremented'))
);
},
{ dispatch: false }
);
constructor(private actions$: Actions) {}
}
The ofType
operator takes multiple actions, and knows how to distinguish each action correctly. Underneath, the operator is looking at the type
property of the action creator, or the type
property on the action class instance. If you need to pass along some metadata with the action, you will need to specific the union types on the Actions
generic.
That's It! To see a working example, see the completed StackBlitz.
Follow me on Twitter, and Twitch. If you like this content, consider sponsoring me on GitHub.