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 typeseach action as a class implementing
Action
interfaceunion 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
- use
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
withon()
functions (state change function)the
on(actionCreator, reducer)
function takes 2 argumentsactionCreator
- the reference to the created actionreducer
- 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()
decoratoruse
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);
}
);