Do you have an awesome application written with Angular v7 using NgRx v7, but have been feeling left out will all the mentions online and at conferences about Angular v8 and NgRx v8? Well you are in luck! Today we will explore together, how to upgrade our applications to use Angular v8 using the Angular CLI tooling. We will also explore upgrading to NgRx v8. This will allow us to take advantage of the new features provided in NgRx v8. Included with NgRx v8 is a shiny set of creators, or type-safe factory functions, for actions, effects, and reducers.
The Angular team has provided a great website that walks through the process of upgrading in-depth. This website can be found at Angular Update Tool. We will touch on some of the information today.
The first step is the process is to upgrade our application to Angular v8. We will use the Angular CLI to manage this process for us.
This is the preferred method, as Angular has provided built-in migration scripts or schematics to alleviate some of the manual process involved had we just simply updated versions in our package.json
.
Let's start by running the following command in the terminal:
Update the Global Angular CLI version
npm install -g @angular/cli
Update the core framework and local CLI to v8
ng update @angular/cli @angular/core
Throughout this process, we might encounter issues with third-party libaries. In those instances, it is best to visit the GitHub issues and repositories for those libraries for resolution.
Now that we have upgraded our application to use Angular v8, let's proceed with updating NgRx to v8. We will make use of the Angular CLI here as well.
Update NgRx to v8
ng update @ngrx/store
The prior command should update our package.json
dependencies and run any NgRx-provided migrations to keep our application in working order.
Depending on your setup, ng update @ngrx/store
may not automatically update the additional @ngrx/*
libraries that you have installed. If this happens, the best course is to manually run npm install
for each additional module in use with NgRx.
Examples are as follows:
npm install @ngrx/entity@latest
npm install @ngrx/effects@latest
npm install @ngrx/data@latest
npm install @ngrx/router-store@latest
The NgRx team has provided a detailed migration guide for updating to NgRx v8. More information on upgrading to v8 of NgRx can be found here: V8 Update Guide
One of the most popular ways to learn new methods, is through code examples. Let's explore the following example of a simplified NgRx store that holds an array
of Fruit
objects.
Each Fruit
object consists of three properties fruitId
, fruitClass
and fruitName
.
interface Fruit {
fruitId: number;
fruitType: string;
fruitName: string;
}
For example, if we had an orange
, it might look something like this:
const orange: Fruit = {
fruitId: 1,
fruitType: 'citrus',
fruitName: 'orange'
};
Exploring further, our State
object in the NgRx store will contain properties like fruits
, isLoading
, and errorMessage
.
fruits
is defined as anarray
forFruit
objectsisLoading
is aboolean
to keep track of when the store is in the process of loading data from an external API.errorMessage
is astring
property that isnull
unless an error has occurred while requesting data from an external API.
An example State
interface
might look like the following:
interface State {
fruits: Fruit[];
isLoading: boolean;
errorMessage: string;
}
An example store with fruits
loaded might look like the following:
const state: State = {
fruits: [
{
fruitId: 1,
fruitType: 'citrus',
fruitName: 'orange'
}
],
isLoading: false,
errorMessage: null
}
Following proper redux pattern guidance, we cannot directly update state, so we need to define a set of actions to work with our state through a reducer. Let's imagine we have 3 actions for this example:
-
[App Init] Load Request
- This action is intended to be dispatched from our UI layer to indicate we are requesting to loadFruit
objects into our store. This action does not have a payload orprops
. -
[Fruits API] Load Success
- This action is intended to be dispatched from our effects when an[App Init] Load Request
has been dispatched, an API has been called and successful response is received from the API. This action contains a payload orprops
object that includes thearray
ofFruit
object to be loaded into our store. -
[Fruits API] Load Failure
- This action is intended to be dispatched from our effects when an[App Init] Load Request
has been dispatched, an API has been called and failure response is received from the API. This action contains a payload orprops
object that includes the error message of our API request, so that it can be loaded into our store.
The actual NgRx v7 implementation of our actions might look something like the following:
import { Action } from '@ngrx/store';
import { Fruit } from '../../models';
export enum ActionTypes {
LOAD_REQUEST = '[App Init] Load Request',
LOAD_FAILURE = '[Fruits API] Load Failure',
LOAD_SUCCESS = '[Fruits API] Load Success'
}
export class LoadRequestAction implements Action {
readonly type = ActionTypes.LOAD_REQUEST;
}
export class LoadFailureAction implements Action {
readonly type = ActionTypes.LOAD_FAILURE;
constructor(public payload: { error: string }) {}
}
export class LoadSuccessAction implements Action {
readonly type = ActionTypes.LOAD_SUCCESS;
constructor(public payload: { fruits: Fruit[] }) {}
}
export type ActionsUnion = LoadRequestAction | LoadFailureAction | LoadSuccessAction;
It's important to note, that while
createAction
is the hot new way of defining anAction
in NgRx, the existing method of defining anenum
,class
and exporting a type union will still work just fine in NgRx v8.
Beginning with version 8 of NgRx, actions can be declared using the new createAction
method. This method is a factory function
, or a function
that returns a function
.
According to the official NgRx documentation, "The createAction
function returns a function, that when called returns an object in the shape of the Action
interface. The props
method is used to define any additional metadata needed for the handling of the action. Action creators provide a consistent, type-safe way to construct an action that is being dispatched."
In order to update to createAction
, we need to do the following steps:
- Create a new
export const
for our action. If our action has a payload, we will also need to migrate to using theprops
method to define our payload asprops
.
Example for [App Init] Load Request
// before
export class LoadRequestAction implements Action {
readonly type = ActionTypes.LOAD_REQUEST;
}
// after
export const loadRequest = createAction('[App Init] Load Request');
Example for [Fruits API] Load Success
// before
export class LoadSuccessAction implements Action {
readonly type = ActionTypes.LOAD_SUCCESS;
constructor(public payload: { fruits: Fruit[] }) {}
}
// after
export const loadSuccess = createAction('[Fruits API] Load Success', props<{fruits: Fruit[]}>());
-
Remove the old action from the
ActionTypes
enum
-
Remove the old action from the
ActionsUnion
Our final migrated actions file might look something like this:
import { Action, props } from '@ngrx/store';
import { Fruit } from '../../models';
export const loadRequest = createAction('[App Init] Load Request');
export const loadFailure = createAction('[Fruits API] Load Failure', props<{errorMessage: string}>());
export const loadSuccess = createAction('[Fruits API] Load Success', props<{fruits: Fruit[]}>());
As we can see, this is a huge reduction in code, we have gone from 24 lines of code, down to 6 lines of code.
A final note is that we need to update the way we dispatch our actions. This is because we no longer need to create class
instances, rather we are calling factory
functions that return an object of our action.
Our before and after will look something like this:
// before
this.store.dispatch(new featureActions.LoadSuccessAction({ fruits }))
// after
this.store.dispatch(featureActions.loadSuccess({ fruits }))
Continuing with our example, we need a reducer setup to broker our updates to the store. Recalling back to the redux pattern, we cannot directly update state. We must, through a pure function, take in current state, an action, and return a new updated state with the action applied. Typically, reducers are large switch
statements keyed on incoming actions.
Let's imagine our reducer handles the following scenarios:
-
On
[App Init] Load Request
we want the state to reflect the following values:state.isLoading: true
state.errorMessage: null
-
On
[Fruits API] Load Success
we want the state to reflect the following values:state.isLoading: false
state.errorMessage: null
state.fruits: action.payload.fruits
-
On
[Fruits API] Load Failure
we want the state to reflect the following values:state.isLoading: false
state.errorMessage: action.payload.errorMessage
The actual NgRx v7 implementation of our reducer might look something like the following:
import { ActionsUnion, ActionTypes } from './actions';
import { initialState, State } from './state';
export function featureReducer(state = initialState, action: ActionsUnion): State {
switch (action.type) {
case ActionTypes.LOAD_REQUEST: {
return {
...state,
isLoading: true,
errorMessage: null
};
}
case ActionTypes.LOAD_SUCCESS: {
return {
...state,
isLoading: false,
errorMessage: null,
fruits: action.payload.fruits
};
}
case ActionTypes.LOAD_FAILURE: {
return {
...state,
isLoading: false,
errorMessage: action.payload.errorMessage
};
}
default: {
return state;
}
}
}
It's important to note, that while
createReducer
is the hot new way of defining a reducer in NgRx, the existing method of defining afunction
with aswitch
statement will still work just fine in NgRx v8.
Beginning with version 8 of NgRx, reducers can be declared using the new createReducer
method.
According to the official NgRx documentation, "The reducer function's responsibility is to handle the state transitions in an immutable way. Create a reducer function that handles the actions for managing the state using the createReducer
function."
In order to update to createReducer
, we need to do the following steps:
- Create a new
const reducer = createReducer
for our reducer. - Convert our
switch
case
statements intoon
method calls. Please note, thedefault
case is handled automatically for us. The first parameter of theon
method is the action to trigger on, the second parameter is a handler that takes instate
and returns a new version ofstate
. If the action providesprops
, a second optional input parameter can be provided. In the example below we will use destructuring to pull the necessary properties out of theprops
object. - Create a new
export function reducer
to wrap ourconst reducer
forAOT
support.
Once completed, our updated featureReducer
will look something like the following:
import { createReducer, on } from '@ngrx/store`;
import * as featureActions from './actions';
import { initialState, State } from './state';
...
const featureReducer = createReducer(
initialState,
on(featureActions.loadRequest, state => ({ ...state, isLoading: true, errorMessage: null })),
on(featureActions.loadSuccess, (state, { fruits }) => ({ ...state, isLoading: false, errorMessage: null, fruits })),
on(featureActions.loadFailure, (state, { errorMessage }) => ({ ...state, isLoading: false, errorMessage: errorMessage })),
);
export function reducer(state: State | undefined, action: Action) {
return featureReducer(state, action);
}
Because we want to keep our reducer a pure function, it's often desirable to place API requests into side-effects
. In NgRx, these are called Effects
and provide a reactive, RxJS-based way to link actions to observable streams.
In our example, we will have an Effect
that listens
for an [App Init] Load Request
Action and makes an HTTP request to our imaginary Fruits API
backend.
-
Upon a successful result from the
Fruits API
the response is mapped to an[Fruits API] Load Success
action setting the payload offruits
to the body of the successful response. -
Upon a failure result from the
Fruits API
the error message is mapped to an[Fruits API] Load Failure
action setting the payload oferrorMessage
to the error from the failure response.
The actual NgRx v7 implementation of our effect might look something like the following:
@Effect()
loadRequestEffect$: Observable<Action> = this.actions$.pipe(
ofType<featureActions.LoadRequestAction>(
featureActions.ActionTypes.LOAD_REQUEST
),
concatMap(action =>
this.dataService
.getFruits()
.pipe(
map(
fruits =>
new featureActions.LoadSuccessAction({
fruits
})
),
catchError(error =>
observableOf(new featureActions.LoadFailureAction({ errorMessage: error.message }))
)
)
)
);
It's important to note, that while
createEffect
is the hot new way of defining a reducer in NgRx, the existing method of defining a class property with an@Effect()
decorator will still work just fine in NgRx v8.
Beginning with version 8 of NgRx, effects can be declared using the new createEffect
method, according to the official NgRx documentation.
In order to update to createEffect
, we need to do the following steps:
- Import
createEffect
from@ngrx/effects
- Remove the
@Effect()
decorator - Remove the
Observable<Action>
type annotation - Wrap
this.actions$.pipe(...)
withcreateEffect(() => ...)
- Remove the
<featureActions.LoadRequestAction>
type annotation fromofType
- Change the
ofType
input parameter fromfeatureActions.ActionTypes.LOAD_REQUEST
tofeatureActions.loadRequest
- Update the action calls to remove
new
and to use the creator instead ofclass
instance. For example,new featureActions.LoadSuccessAction({fruits})
becomesfeatureActions.loadSuccess({fruits})
.
Once completed, our updated loadRequestEffect
will look something like the following:
loadRequestEffect$ = createEffect(() => this.actions$.pipe(
ofType(featureActions.loadRequest),
concatMap(action =>
this.dataService
.getFruits()
.pipe(
map(fruits => featureActions.loadSuccess({fruits})),
catchError(error =>
observableOf(featureActions.loadFailure({ errorMessage: error.message }))
)
)
)
)
);
This brings us to the end of this guide. Hopefully you've been able to learn about upgrading your application to Angular v8 and NgRx v8. In addition, you should feel confident in taking advantage of some of the new features available in NgRx v8 to reduce the occurrence of what some might refer to as boilerplate. Happy updating and upgrading!
Thanks! At the time of this article
of
threw a deprecation warning inrxjs
. As a workaround I had to aliasof
asobservableOf
. That has since been resolved in a later build ofrxjs
. You can just useof
.