Migrate legacy NGRX

Before version 15 of NGRX, some old syntax and decorators were allowed but deprecated. I'll show you how to migrate a simple, but deprecated code to NGRX@15. The following example is mostly taken from a real-world application. There will be minor code refactoring included.

Actions

The old way of creating actions.

  • an enum CountriesActionTypes with action types

  • each action as a class implementing Action interface

  • union type CountriesActions with action class' references

import { Action } from '@ngrx/store';

export enum CountriesActionTypes {
  LoadCountries = '[Countries] Load Countries',
  LoadCountriesSuccess = '[Countries] Load Countries Success',
  LoadCountriesFailure = '[Countries] Load Countries Failure',
}

export class LoadCountries implements Action {
  readonly type = CountriesActionTypes.LoadCountries;
}

export class LoadCountriesSuccess implements Action {
  readonly type = CountriesActionTypes.LoadCountriesSuccess;

  constructor(public payload: string[]) {
  }
}

export class LoadCountriesFailure implements Action {
  readonly type = CountriesActionTypes.LoadCountriesFailure;
}

export type CountriesActions = LoadCountries | LoadCountriesSuccess | LoadCountriesFailure;

To migrate:

  • remove the enum with action types

  • replace action classes with createAction() functions

    • use props() function to define action payload
import { createAction, props } from '@ngrx/store';


export const requestCountries = createAction('[Countries] Request countries');
export const requestCountriesSuccess = createAction('[Countries] Request countries success', props<{ response: string[] }>());
export const requestCountriesError = createAction('[Countries] Request countries error', props<{ error: unknown }>());

Reducer

The old reducer uses switch instruction to update state according to supported action type (taken from action's class type property).

import { CountriesActions, CountriesActionTypes } from './countries.actions';


export const STATE_NAME = 'countries';

export interface CountriesState {
  loading?: boolean;
  error?: boolean;
  list?: string[];
}

export function countriesReducer(state: CountriesState = {}, action: CountriesActions): CountriesState {
  switch (action.type) {
    case CountriesActionTypes.LoadCountries:
      return {
        ...state,
        loading: true,
        error: false,
      };
    case CountriesActionTypes.LoadCountriesSuccess:
      return {
        ...state,
        list: action.payload,
        loading: false,
        error: false,
      };
    case CountriesActionTypes.LoadCountriesFailure:
      return {
        ...state,
        loading: false,
        error: true,
      };
    default:
      return state;
  }
}

To migrate:

  • (minor) update state key names to be more verbose

  • introduce initialState

  • update createReducer function:

    • pass initialState

    • replace switch with on() functions (state change function)

      • the on(actionCreator, reducer) function takes 2 arguments

        • actionCreator - the reference to the created action

        • reducer - a function that takes the current state and current action and returns the updated state (state, action) => ({...})

import { Action, createReducer, on } from '@ngrx/store';
import {
  requestCountries,
  requestCountriesError,
  requestCountriesSuccess,
} from './countries.actions';

export const STATE_NAME = 'countries';

export interface State {
  countriesResponse?: string[];
  countriesLoading: boolean;
  countriesError: unknown;
}

const initialState: State = {
  countriesResponse: undefined,
  countriesLoading: false,
  countriesError: undefined,
};

const reducer = createReducer(
  initialState,

  on(requestCountries, (state) => ({
    ...state,
    countriesResponse: undefined,
    countriesLoading: true,
    countriesError: undefined,
  })),
  on(requestCountriesError, (state, {error}) => ({
    ...state,
    countriesLoading: false,
    countriesError: error,
  })),
  on(requestCountriesSuccess, (state, action) => ({
    ...state,
    countriesResponse: action.response,
    countriesLoading: false,
  })),
);

export function countriesReducer(state: State | undefined, action: Action) {
  return reducer(state, action);
}

Effects

Before version 7, you can find the usage of ofType function chained directly on this.actions$ (injected Actions). This syntax was dropped in version 7 in favor of ofType operator.

@Injectable()
export class CountriesEffects {
  @Effect()
  someEffect$: Observable<Action> = this.actions$
    .ofType(CountriesActionTypes.LoadCountries)
    .pipe(
      map((response) => new LoadCountriesSuccess(response)),
      catchError(() => of(new LoadCountriesFailure()))
    );

  constructor(private actions$: Actions) {}
}

The old effects use @Effect decorator which was replaced by createEffect() function.

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, of } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { CountriesActionTypes, LoadCountriesFailure, LoadCountriesSuccess } from './countries.actions';
import { CountriesService } from './countries.service';

@Injectable()
export class CountriesEffects {
  @Effect()
  load: Observable<LoadCountriesSuccess | LoadCountriesFailure> = this.actions.pipe(
    ofType(CountriesActionTypes.LoadCountries),
    switchMap(() =>
      this.countriesService.list().pipe(
        map((response: string[]) => new LoadCountriesSuccess(response)),
        catchError(() => of(new LoadCountriesFailure()))
      )
    )
  );



  constructor(
    private actions: Actions,
    private countriesService: CountriesService
  ) {
  }
}

To migrate:

  • use createEffect() function instead of @Effect() decorator

  • use ofType() operator to filter the desired action

  • (minor) rename the effect property to be more verbose and end it with $ which indicates that it's an observable

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, mergeMap, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
import {
  requestCountries,
  requestCountriesError,
  requestCountriesSuccess,
} from './countries.actions';
import { CountriesService } from './countries.service';

@Injectable()
export class CountriesEffects {
  requestCountries$ = createEffect(() =>
    this.actions$.pipe(
      ofType(requestCountries),
      mergeMap((action) =>
        this.countriesService.getCountries().pipe(
          switchMap((response) => of(requestCountriesSuccess({response}))),
          catchError((error) => of(requestCountriesError({error})))
        )
      )
    )
  );

  constructor(
    private readonly actions$: Actions,
    private readonly countriesService: CountriesService,
  ) {
  }
}

Selectors

If you were using selectors with props they might also need migration. You have to replace them with factory selectors.

const selectFirstNCountries = createSelector(
    selectCountries,
    (countries, props: { count: number }) => {
      return countries?.slice(count);
    }
);

To migrate:

  • create a factory with a parameter instead of a selector with props
const selectFirstNCountries = (count: number) =>
  createSelector(
    selectCountries,
    (countries) => {
      return countries?.slice(count);
    }
  );