Brandon Roberts headshot rounded

Brandon Roberts

Notes to my future self

Mixing Action Styles in NgRx State

May 14, 2020 - 6 min read

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

via GIPHY

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.