How to upgrade your Angular and NgRx Apps to v8

Wes - Jun 14 '19 - - Dev Community

Overview

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.

This article has been adapted from an original post on Ultimate Courses.

Upgrading Dependencies

Upgrading Angular

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
Enter fullscreen mode Exit fullscreen mode

Update the core framework and local CLI to v8

ng update @angular/cli @angular/core
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

NgRx Migration Guide

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

Learn by Example - a Fruit Store (NgRx v7)

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;
}
Enter fullscreen mode Exit fullscreen mode

For example, if we had an orange, it might look something like this:

const orange: Fruit = {
    fruitId: 1,
    fruitType: 'citrus',
    fruitName: 'orange'
};
Enter fullscreen mode Exit fullscreen mode

State

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 objects
  • isLoading 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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Actions

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.

NgRx v7 Implementation

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;
Enter fullscreen mode Exit fullscreen mode

NgRx v8 - Upgrading to createAction

It's important to note, that while createAction is the hot new way of defining an Action in NgRx, the existing method of defining an enum, 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:

  1. Create a new 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;
}
Enter fullscreen mode Exit fullscreen mode
// after
export const loadRequest = createAction('[App Init] Load Request');
Enter fullscreen mode Exit fullscreen mode

Example for [Fruits API] Load Success

// before
export class LoadSuccessAction implements Action {
  readonly type = ActionTypes.LOAD_SUCCESS;
  constructor(public payload: { fruits: Fruit[] }) {}
}
Enter fullscreen mode Exit fullscreen mode
// after
export const loadSuccess = createAction('[Fruits API] Load Success', props<{fruits: Fruit[]}>());
Enter fullscreen mode Exit fullscreen mode
  1. Remove the old action from the ActionTypes enum

  2. 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[]}>());
Enter fullscreen mode Exit fullscreen mode

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.

NgRx v8 - Dispatching createAction Actions

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 }))
Enter fullscreen mode Exit fullscreen mode

Reducer

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

NgRx v7 Implementation

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

NgRx v8 - Upgrading to createReducer

It's important to note, that while createReducer is the hot new way of defining a reducer in NgRx, the existing method of defining a function with a switch 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:

  1. Create a new const reducer = createReducer for our reducer.
  2. Convert our 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.
  3. Create a new 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);
}
Enter fullscreen mode Exit fullscreen mode

Effects

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 of fruits 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 of errorMessage to the error from the failure response.

NgRx v7 Implementation

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 }))
          )
        )
    )
  );
Enter fullscreen mode Exit fullscreen mode

NgRx v8 - Upgrading to createEffect

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:

  1. Import createEffect from @ngrx/effects
  2. Remove the @Effect() decorator
  3. Remove the Observable<Action> type annotation
  4. Wrap this.actions$.pipe(...) with createEffect(() => ...)
  5. Remove the <featureActions.LoadRequestAction> type annotation from ofType
  6. Change the ofType input parameter from featureActions.ActionTypes.LOAD_REQUEST to featureActions.loadRequest
  7. Update the action calls to remove 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 }))
                )
            )
        )
    )
  );
Enter fullscreen mode Exit fullscreen mode

Full Video Walkthrough

If you would like to watch a full video walkthrough here you go.

Conclusion

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .