Updating Your Angular App to NgRx 8

Jul 01, 2019 reading time 9 minutes

Updating Your Angular App to NgRx 8

In this blog post I want to describe how I updated an Angular Project using ngrx to the latest version, ngrx 8.

GitHub: angular-ngrx-todo

You can find the complete update commit here : angular-ngrx-todo update commit

  • Preparations
  • Updating the actions
  • Updating the effects
  • Updating the reducer
  • Updating the components

Preparations

I started updating the project with update.angular.io and came to update the store with

ng update @ngrx/store

This will lift your version of @ngrx/store and @ngrx/effects to version 8

Before:

"@ngrx/effects": "^7.3.0",
"@ngrx/store": "^7.3.0",

After:

"@ngrx/effects": "^8.0.1",
"@ngrx/store": "^8.0.1",

If it des not update the related packages automatically, you can easily run

npm install @ngrx/entity@latest
npm install @ngrx/effects@latest
npm install @ngrx/data@latest
npm install @ngrx/router-store@latest

by yourself.

Updating the actions

What I maybe love the most about the new ngrx version is the createAction(...) method which can be imported from @ngrx/store

import { createAction, props } from '@ngrx/store';

With this method we can create actions instead of the old style

export enum ActionTypes {
  LoadAllTodos = '[Todo] Load Todos',
  LoadAllTodosFinished = '[Todo] Load Todos Finished',

  //...many more actions
}

export class LoadAllTodosAction implements Action {
  readonly type = ActionTypes.LoadAllTodos;
}

export class LoadAllTodosFinishedAction implements Action {
  readonly type = ActionTypes.LoadAllTodosFinished;
  constructor(public payload: Todo[]) {}
}

//...many more actions

export type TodoActions =
  | LoadAllTodosAction
  | LoadAllTodosFinishedAction;

With a new style which takes less less code:

export const loadAllTodos = createAction('[Todo] Load Todos');

export const loadAllTodosFinished = createAction(
  '[Todo] Load Todos Finished',
  props<{ payload: Todo[] }>()
);

That’s it. No enums or summarized types anymore, its a call to createAction() either with one parameter as a string describing your action or with a second parameter describing what payload the action should take. These properties are introduced with the generic props<...> type which takes an object holding all your payload parameters.

As a side note: In other files I imported all the actions directly afterwards from that action file I did like:

import {
  ActionTypes,
  AddTodoAction,
  AddTodoFinishedAction,
  LoadAllTodosFinishedAction,
  LoadSingleTodoAction,
  LoadSingleTodoFinishedAction,
  SetAsDoneAction,
  SetAsDoneFinishedAction,
  DeleteTodoAction,
  DeleteTodoFinishedAction
} from './todo.actions';

Which I do now via a oneliner and renaming the import like

import * as todoActions from './todo.actions';

IMHO I think this is easier to handle.

Updating the effects

Before in the old versions we piped into the action stream and filtered the actions which are interesting for that effect with the ofType decorator like this:

  @Effect()
  loadTodos$ = this.actions$.pipe(
    ofType(ActionTypes.LoadAllTodos),
    switchMap(() =>
      this.todoService
        .getItems()
        .pipe(
          map(
            (todos: Todo[]) => new LoadAllTodosFinishedAction(todos),
            catchError(error => of(error))
          )
       )
    )
  );

Now effects are created with the createEffect(...) method which can also be imported from import { ..., createEffect } from '@ngrx/effects';. Also the @Effect() Decorator is gone. Having done that we are calling the method with the appropriate fat arrow function to pass

import { Actions, ofType, createEffect } from '@ngrx/effects';

...

  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(todoActions.loadAllTodos),
      switchMap(action =>
        this.todoService.getItems().pipe(
          map(todos => todoActions.loadAllTodosFinished({ payload: todos })),
          catchError(error => of(error))
        )
      )
    )
  );

Please note that I named the parameter in the created action payload and therefore I have to create a new object and fill the parameter payload with the result of the service method which I named todos here.

map(todos => todoActions.loadAllTodosFinished({ payload: todos })),

If you would create the action like

export const loadAllTodosFinished = createAction(
  '[Todo] Load Todos Finished',
  props<{ todos: Todo[] }>()
);

you could use the short term syntax in the effect like

map(todos => todoActions.loadAllTodosFinished({ todos })),

here. But that is for the record.

Updating the reducer

Another really nice part is to update the reducer. The reducer was often called old school because of that massive switch/case statement. This is gone now.

So the old switch/case

export function myReducer(
  state = initialState,
  action
): State {
  switch (action.type) {
    case action1: {
      return { ... };
    }

    case action2: {
      return { ... };
    }

    case action3: {
      return { ... };
    }

    default: {
      return state;
    }
  }
}

turns to a new usage of the on(...) method

import { createReducer, on, Action } from '@ngrx/store';

...

const myReducer = createReducer(
  initialState,
  on(action1, state => ({ ... })),
  on(action2, state => ({ ... })),
  on(action3, state => ({ ... })),
);

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

So in the conrete case of loading todos we had a state like this:

export interface ReducerTodoState {
  items: Todo[];
  selectedItem: Todo;
  loading: boolean;
}

export const initialState: ReducerTodoState = {
  items: [],
  selectedItem: null,
  loading: false
};

which was untouched. The reducer was created with the createReducer(...) method passing it the initialstate as a first argument.

const todoReducerInternal = createReducer(
  initialState,
  ...
);

The default case is automatically returned, it needs no statement anymore.

With the help of the on(...) method we can now handle all the actions we are interested in:

import * as todoActions from './todo.actions';

...

const todoReducerInternal = createReducer(
  initialState,
  on(
    todoActions.loadAllTodos,
    state => ({
      ...state,
      loading: true
    })
  ),
  on(todoActions.loadAllTodosFinished, (state, { payload }) => ({
    ...state,
    loading: false,
    items: [...payload]
  })),
  on(..., (state, { payload }) => ({
    ...state,
    ... // more stuff
  })),
);

So the complete old reducer was:

export function todoReducer(
  state = initialState,
  action: TodoActions
): ReducerTodoState {
  switch (action.type) {
    case ActionTypes.AddTodo:
    case ActionTypes.LoadAllTodos:
    case ActionTypes.LoadSingleTodo:
    case ActionTypes.SetAsDone: {
      return {
        ...state,
        loading: true
      };
    }

    case ActionTypes.AddTodoFinished: {
      return {
        ...state,
        loading: false,
        items: [...state.items, action.payload]
      };
    }

    case ActionTypes.LoadAllTodosFinished: {
      return {
        ...state,
        loading: false,
        items: [...action.payload]
      };
    }

    case ActionTypes.LoadSingleTodoFinished: {
      return {
        ...state,
        loading: false,
        selectedItem: action.payload
      };
    }

    case ActionTypes.SetAsDoneFinished: {
      const index = state.items.findIndex(x => x.id === action.payload.id);

      state.items[index] = action.payload;

      return {
        ...state,
        loading: false
      };
    }

    default:
      return state;
  }
}

You can see in the github that I had multiple actions in the switch/case statement as I wanted to activate the loading boolean as reaction to mutiple actions. Of course you can do this with the on(...) method, too. So my new reducer is:

const todoReducerInternal = createReducer(
  initialState,
  on(
    todoActions.addTodo,
    todoActions.deleteTodo,
    todoActions.loadAllTodos,
    todoActions.loadSingleTodo,
    todoActions.setAsDone,
    state => ({
      ...state,
      loading: true
    })
  ),
  on(todoActions.addTodoFinished, (state, { payload }) => ({
    ...state,
    loading: false,
    items: [...state.items, payload]
  })),
  on(todoActions.loadAllTodosFinished, (state, { payload }) => ({
    ...state,
    loading: false,
    items: [...payload]
  })),
  on(todoActions.loadSingleTodoFinished, (state, { payload }) => ({
    ...state,
    loading: false,
    selectedItem: payload
  })),
  on(todoActions.deleteTodoFinished, (state, { payload }) => ({
    ...state,
    loading: false,
    items: [...state.items.filter(x => x !== payload)]
  })),
  on(todoActions.setAsDoneFinished, (state, { payload }) => {
    const index = state.items.findIndex(x => x.id === payload.id);

    state.items[index] = payload;

    return {
      ...state
    };
  })
);

Updating the components

As we have no Action Enum anymore and working directly with the methods created we have to update our components as well. Before we created a new class of the action with new xyzAction(...) or new xyz(...) like

import {
  LoadAllTodosAction
} from '@app/todo/store/todo.actions';


ngOnInit() {
    this.store.dispatch(new LoadAllTodosAction());
}

Now we can directly use that action without the new keyword anymore

import * as fromTodoStore from '@app/todo/store';

ngOnInit() {
    this.store.dispatch(fromTodoStore.loadAllTodos());
}

If you want to pass parameters to that action because it is expecting some you can easily to that by passing them directly into that function but wrapped in an object!

addTodo(item: string) {
    this.store.dispatch(fromTodoStore.addTodo({ payload: item }));
}

Again: If you would name the parameter item instead of payload you could use the short term syntax like this.store.dispatch(fromTodoStore.addTodo({ item }));

And that is it! You can check the links at the beginning of this post. This Post was also inspired by this post from @wesgrimes and this twitter conversation

I hope this helps anyone.

Fabian