How to upgrade your Angular and NgRx Apps to v8 - 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.## Upgrading NgRx
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 an array
for Fruit
objectsisLoading
is a boolean
to keep track of when the store is in the process of loading data from an external API.errorMessage
is a string
property that is null
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 load Fruit
objects into our store. This action does not have a payload or props
.[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 or props
object that includes the array
of Fruit
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 or props
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 newcreateAction
method. This method is afactory function
, or afunction
that returns afunction
.
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:
export const
for our action. If our action has a payload, we will also need to migrate to using the props
method to define our payload as props
.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[]}>());
ActionTypes
enum
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:
[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
### NgRx v7 ImplementationThe 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 newcreateReducer
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:
const reducer = createReducer
for our reducer.switch
case
statements into on
method calls. Please note, the default
case is handled automatically for us. The first parameter of the on
method is the action to trigger on, the second parameter is a handler that takes in state
and returns a new version of state
. If the action provides props
, a second optional input parameter can be provided. In the example below we will use destructuring to pull the necessary properties out of the props
object.export function reducer
to wrap our const reducer
for AOT
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.
Fruits API
the response is mapped to an [Fruits API] Load Success
action setting the payload of fruits
to the body of the successful response.Fruits API
the error message is mapped to an [Fruits API] Load Failure
action setting the payload of errorMessage
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 newcreateEffect
method, according to the official NgRx documentation.
In order to update to createEffect
, we need to do the following steps:
createEffect
from @ngrx/effects
@Effect()
decoratorObservable<Action>
type annotationthis.actions$.pipe(...)
with createEffect(() => ...)
<featureActions.LoadRequestAction>
type annotation from ofType
ofType
input parameter from featureActions.ActionTypes.LOAD_REQUEST
to featureActions.loadRequest
new
and to use the creator instead of class
instance. For example, new featureActions.LoadSuccessAction({fruits})
becomes featureActions.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 }))
)
)
)
)
);
If you would like to watch a full video walkthrough here you go.
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!
#angular