{ "community_link": "https://community.indepth.dev/t/adding-ngrx-to-your-existing-applications/247" }

Adding NgRx to Your Existing Applications

If you’re currently considering adding NgRx to your project, or just want to learn, this piece is going to cover the basics of the flux pattern and how to use it.

Adding NgRx to Your Existing Applications

State management and the “flux” pattern are hot topics in web development today. All of the leading frameworks have a variation with React using Redux, Angular using NgRx, VueJS using Vuex, and others. NgRx in particular has become very popular in the Angular community. If you’re currently considering adding NgRx to your project, or just want to learn, this piece is going to cover the basics of the flux pattern and how to use it. I’m going to walkthrough modifying an Angular application from using service calls to NgRx.

Throughout this post, you’ll learn some basics about NgRx actions, reducers, selectors, and effects.

In my examples I’ll also be using NgRx’s create functions that were released with version 8. These are great because they greatly reduce boilerplate code that had traditionally been seen in NgRx implementations. For a more in depth presentation of how the create functions work, I highly recommend Tim Deschryver’s post “NgRx Creator Functions 101” here.

This post is also following a PR that I’ve opened on the NgRx project to add a section to their docs covering how to modify an existing application to use NgRx (you can see my PR here).

The project we are going to walkthrough can be reached on GitHub here. It is a basic “to-do” application. It has one “to-do” service, and I will be walking through it in the next sections.

The Project

Before we begin, it helps to understand the project we’re going to be working with.

The project “to-do-with-ngrx” just uses your browser’s localStorage to create, edit, and delete a set of “to-do” items. The application is super simple and only has one component with one “to-do” service.

The “to-do service” looks like the following:

import { Injectable } from '@angular/core';
import { Item } from '../models/item';
@Injectable({
  providedIn: 'root'
})
export class ToDoService {

    constructor() {}
    
    getItems() {
        let items = JSON.parse(window.localStorage.getItem('items'));
        if (items === null) {
          items = [];
        }
        return items;
      }

    addItem(addItem: string) {
        const itemsStored = window.localStorage.getItem('items');
        let items = [];
        if (itemsStored !== null) {
          items = JSON.parse(itemsStored);
        }
        const item: Item = {
          id: items.length + 1,
          name: addItem
        };
        items.push(item);
        window.localStorage.setItem('items', JSON.stringify(items));
      }

    deleteItem(deleteItem) {
        const items = JSON.parse(window.localStorage.getItem('items'));
        console.log(items);
        console.log(deleteItem);
        const saved = items.filter(item => {
          return item.id !== deleteItem.id;
        });
        window.localStorage.setItem('items', JSON.stringify(saved));
      }
}

As you can see, the methods here just use the browser’s localStorage to save the “to-do items.” Without NgRx, the application uses these methods through direct calls in the component. We’re going to change the application to use these same service methods through actions, reducers, selectors, and effects. This will make the application easier to maintain, and enable you to more easily manage its state.

NgRx Preview

Before we walk though the process of refactoring the application, it helps to understand some basics about NgRx.

I borrowed this diagram from my article How to Start Flying with Angular and NgRx article for this discussion.

The basic premise is that you have to consider any changes in your application as changes in state. This can include anything from literal data changes to things like a button being clicked or enabled.

It also helps to understand the flux pattern if you consider change in terms of mutable and immutable.

  • mutable meaning that your application’s state can be changed after it was created. This is what you normally see with service based architectures.
  • immutable means that your application’s state does not change after creation. However, with the flux pattern (and NgRx) your application’s state can be computed (or replaced) through mutators called reducers.

With flux (and NgRx), your applications state is managed in a central location and becomes the single source of truth. The actual application state is maintained through the NgRx store, actions, reducers, selectors, and effects.

  • store = where your application state is maintained.
  • actions = events that are emitted that either create new actions, interact with the store via reducers, or generate side effects
  • reducers = functions that compute new state based on input from actions. State can be mutated with reducers, but it is preserved through complete updates to the store rather than individual updates to different slices of state (preserving immutability).
  • selectors = how your application receives (subscribes) to state from the store.
  • effects = events that call external services and then return corresponding actions. You can think of effects as basically ways to for your application to interact with external services or APIs needed.

This is a lot of vocabulary, but it makes more sense when you consider how individual changes to your application are handled via direct service calls. When your application does this, it can make debugging difficult because you have to manually create the flow that generated the changes.

Using the flux pattern (and NgRx), any state change is actually a complete replacement of the state with any newly computed values. So you can maintain change history, and clearly see the results of different interactions as users interact with your components.

The power of the flux pattern (and NgRx) is that it enables you to have a consistent pattern that can be followed throughout your application. The same methods of state change can all be handled via the use of the store, actions, reducers, selectors, and effects. This makes development more standardized, and makes maintenance less difficult. It also provides a way to track changes in your application through the different events and flows defined.

Installing NgRx and Scaffolding the Application

So as I stated in the intro, I’m going to walkthrough adding NgRx to an Angular application. NgRx is Angular’s version of Redux, and has a lot of community support.

To start, we’l need to do a git clone of my sample project. I’ve stored a version of the project that doesn’t have NgRx in the “before-ngrx” branch. A git clone of this branch can be done with the following:

git clone --branch before-ngrx https://github.com/andrewevans0102/to-do-with-ngrx.git
You can also skip ahead to a complete version of the application by doing "git clone --branch after-ngrx https://github.com/andrewevans0102/to-do-with-ngrx.git"

With the project cloned, open up the directory in your terminal and do the standard npm install and then run npm run serve to run it locally.

To add NgRx to this project, we’re going to be installing the following:

To install these do the following:

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools --save

Now with these installed, in the src/app folder create the following files:

  • ToDoActions.ts
  • ToDoEffects.ts
  • ToDoReducers.ts
Normally, you would create these next to the components that they support. Since this application only has one component, they all reside next to the standard Angular app component that is built when the project is created.

Next, connect this to the app.module file by modifying it to be what you see here:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
import { EffectsModule } from '@ngrx/effects';
import { ToDoEffect } from './ToDoEffects';
import { StoreModule } from '@ngrx/store';
import { ToDoReducer } from './ToDoReducers';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    StoreModule.forRoot({ toDo: ToDoReducer }),
    EffectsModule.forRoot([ToDoEffect]),
    StoreDevtoolsModule.instrument({
      maxAge: 25,
      logOnly: environment.production
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

If you notice in particular the following:

StoreModule.forRoot({ toDo: ToDoReducer }),
EffectsModule.forRoot([ToDoEffect]),
StoreDevtoolsModule.instrument({
  maxAge: 25,
  logOnly: environment.production
})

Here we are registering our store, effects, and reducers so that the instance of the application is aware of them.

Now that the application is scaffolded let’s go ahead and add the nuts and bolts that make it work.

Actions

So in our application, we have basic needs that include:

  • Loading the items for the page
  • Adding a to-do item
  • Deleting a to-do item
  • returning an error when it occurs

Go ahead and copy and paste the following into the ToDoActions.ts file:

import { createAction, props } from '@ngrx/store';
import { Item } from './models/item';
export const getItems = createAction('[to-do] get items');

export const loadItems = createAction(
  '[to-do] load items',
  props<{ items: Item[] }>()
);

export const addItem = createAction(
  '[to-do] add item',
  props<{ name: string }>()
);

export const deleteItem = createAction(
  '[to-do] delete item',
  props<{ item: Item }>()
);

export const errorItem = createAction(
  '[to-do] error item',
  props<{ message: string }>()
);

If you notice, the actions defined here all correspond to the different things we’ll need to do with our application. We’re using the create functions which reduces boilerplate and makes it easier to understand. The syntax for these definitions is basically just using the createAction function and then passing (1) the action name and then (2) any optional props (or payload) that is to be sent when the action fires.

As I mentioned in the intro, for a more in depth walkthrough of this syntax I highly recommend Tim Deschryver’s post “NgRx Creator Functions 101” here.

Reducers

With the actions defined, we can now add reducers to handle the state changes from our actions.

Go ahead and copy and paste the following into the ToDoReducers.ts file:

import { loadItems, errorItem } from './ToDoActions';
import { on, createReducer } from '@ngrx/store';
import { Item } from './models/item';

export interface State {
  toDo: { items: Item[]; error: string };
}

export const initialState: State = {
  toDo: { items: [], error: '' }
};

export const ToDoReducer = createReducer(
  initialState,
  on(loadItems, (state, action) => ({
    ...state,
    items: action.items
  })),
  on(errorItem, (state, action) => ({
    ...state,
    error: action.message
  }))
);

export const selectItems = (state: State) => state.toDo.items;

export const selectError = (state: State) => state.toDo.error;

So here we are doing a few things. The main purpose of the reducers is to setup behavior when working with state.

We define a global state object with:

export interface State {
  toDo: { items: Item[]; error: string };
}

export const initialState: State = {
  toDo: { items: [], error: '' }
};

Then we define reducers for the [to-do] load items and [to-do] error item actions with:

export const ToDoReducer = createReducer(
  initialState,
  on(loadItems, (state, action) => ({
    ...state,
    items: action.items
  })),
  on(errorItem, (state, action) => ({
    ...state,
    error: action.message
  }))
);

Then at the last section we define selectors which will enable us to retrieve a slice of state with:

export const selectItems = (state: State) => state.toDo.items;

export const selectError = (state: State) => state.toDo.error;

Shouldn’t there be a reducer for each action? Not exactly. We only need reducers for handling when we intend to create a new state in the store. The reducers I’ve defined here are connected to specific actions where we expect a state change to result.

Actual state changes in the “to-do” application only occur when (1) items are loaded by calling the service method or (2) an error occurs. The rest of the actions will have associated side effects which then return actions that compute new state via the reducers. You’ll see this more in the next section.

Effects

With the actions and reducers defined, we need to define events that happen as a result of the actions. In NgRx (and redux) this is called effects .

Go ahead and copy and paste the following into ToDoEffects.ts :

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { addItem, getItems, deleteItem } from './ToDoActions';
import { switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ToDoService } from './services/to-do.service';
@Injectable()
export class ToDoEffect {
      loadItems$ = createEffect(() =>
        this.actions$.pipe(
          ofType(getItems),
          switchMap(action => {
            const itemsLoaded = this.toDoService.getItems();
            return of({ 
              type: '[to-do] load items', items: itemsLoaded 
            });
          }),
          catchError(error => of({ 
            type: '[to-do] error item', message: error 
          }))
        )
      );
  
    addItem$ = createEffect(() =>
        this.actions$.pipe(
          ofType(addItem),
          switchMap(action => {
            this.toDoService.addItem(action.name);
            const itemsLoaded = this.toDoService.getItems();
            return of({ 
              type: '[to-do] load items', items: itemsLoaded 
            });
          }),
          catchError(error => of({ 
            type: '[to-do] error item', message: error 
          }))
        )
      );

    deleteItem$ = createEffect(() =>
        this.actions$.pipe(
          ofType(deleteItem),
          switchMap(action => {
            this.toDoService.deleteItem(action.item);
            const itemsLoaded = this.toDoService.getItems();
            return of({ 
              type: '[to-do] load items', items: itemsLoaded 
            });
          }),
          catchError(error => of({ 
            type: '[to-do] error item', message: error 
          }))
        )
      );

    constructor(
        private actions$: Actions, 
        private toDoService: ToDoService
      ) {}
}

If you notice we’ve just created the effects for:

  • loadItems$ = createEffect(() = calls the “to-do service” to retrieve items
  • addItem$ = createEffect(()= calls the “to-do service” to add an item, then calls “to-do” service to retrieve items (will eventually update state)
  • deleteItem$ = createEffect(() = calls the “to-do service” to delete an item, then calls the “to-do” service to retrieve items (will eventually update state)

As I mentioned in the review section, these are considered “side effects” in NgRx. You can see this shown by the action [to-do] add item. When this action is dispatched (or fired), the associated effect calls the “to-do” service and then generates associated state changes with a return  of [to-do] load items just as you see here:

addItem$ = createEffect(() =>
  this.actions$.pipe(
    ofType(addItem),
    switchMap(action => {
      this.toDoService.addItem(action.name);
      const itemsLoaded = this.toDoService.getItems();
      return of({
        type: '[to-do] load items',
        items: itemsLoaded
      });
    }),
    catchError(error =>
      of({
        type: '[to-do] error item',
        message: error
      })
    )
  )
);

The [to-do] load items that is returned then calls the the “ToDoReducer” with the on(loadItems… and new state is computed. Any components that are subscribed to the store receive the new state via the selectors.

There is a similar flow when items are deleted, but I’ve left that an explanation of that off here for brevity.

Modifying the Application Component

So at this point all of the pieces are defined. However, we’ll need to modify the application component to use the actions and selectors we defined.

Modify the src/application.component.ts to look like this:

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { Item } from './models/item';
import { Store, select } from '@ngrx/store';
import { getItems, addItem, deleteItem } from './ToDoActions';
import { Observable } from 'rxjs';
import { selectItems, selectError } from './ToDoReducers';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

    toDoForm = new FormGroup({
      name: new FormControl('')
    });
    items$: Observable<any>;
    error$: Observable<any>;
  
    constructor(private store: Store<{ toDo: { items: Item[] } }>) {
        this.store.dispatch(getItems());
        this.items$ = this.store.pipe(select(selectItems));
        this.error$ = this.store.pipe(select(selectError));
      }

    onSubmit() {
        this.store.dispatch(
          addItem({ name: this.toDoForm.controls.name.value })
        );
        this.toDoForm.controls.name.reset();
      }

    deleteItem(deleted: Item) {
        this.store.dispatch(deleteItem({ item: deleted }));
      }
}

Notice first the NgRx imports:

import { Store, select } from '@ngrx/store';
import { getItems, addItem, deleteItem } from './ToDoActions';
import { Observable } from 'rxjs';
import { selectItems, selectError } from './ToDoReducers';

This is pulling in the actions and selectors we defined earlier.

Notice also the observables:

items$: Observable<any>;
error$: Observable<any>;

These will subscribe to the store to receive any state changes.

Also notice the constructor:

constructor(private store: Store<{ toDo: { items: Item[] } }>) {
    this.store.dispatch(getItems());
    this.items$ = this.store.pipe(select(selectItems));
    this.error$ = this.store.pipe(select(selectError));
  }

Here we are retrieving the items initially on load, and then creating subscriptions to the items and error values.

Finally, notice the change toonSubmit and deleteItem here:

onSubmit() {
  this.store.dispatch(addItem({ 
    name: this.toDoForm.controls.name.value }));
  this.toDoForm.controls.name.reset();
}

deleteItem(deleted: Item) {
  this.store.dispatch(deleteItem({ item: deleted }));
}

Both of these methods previously made direct service calls, now they dispatch actions. In both cases the actions dispatch, then “side effects” call the “to-do” service methods and create new state in the store with firing the [to-do] load items actions.

With the component modified, the last step is to modify the HTML template to receive the updates from the store:

<header class="c-header">
  <h1 class="c-header__title">To-Do list with NgRx</h1>
  <p class="c-header__subtitle">Learn how to use NgRx with a to-do list</p>
</header>
<section class="c-error" *ngIf="error$ | async as error">
  <h1>{{ error }}</h1>
</section>
<form class="c-form" [formGroup]="toDoForm" (ngSubmit)="onSubmit()">
  <input
    class="c-form__input"
    type="text"
    formControlName="name"
    placeholder="to-do item"
    required
  />
  <button class="c-form__button" type="submit" [disabled]="!toDoForm.valid">
    Create Item
  </button>
</form>
<section class="c-list" *ngIf="items$ | async as items">
  <ul *ngFor="let item of items">
    <li class="c-list__item">
      <button class="c-list__button" (click)="deleteItem(item)">X</button>
      <span class="c-list__item-label">{{ item.name }}</span>
    </li>
  </ul>
</section>

First notice the section that listens for values from the items observable:

<section class="c-list" *ngIf="items$ | async as items">
  <ul *ngFor="let item of items">
    <li class="c-list__item">
      <button class="c-list__button" (click)="deleteItem(item)">X</button>
      <span class="c-list__item-label">{{ item.name }}</span>
    </li>
  </ul>
</section>

Notice also the section that was added to receive any error messages:

<section class="c-error" *ngIf="error$ | async as error">
  <h1>{{ error }}</h1>
</section>

These modifications enable the component template to receive state changes via selectors (subscribing to the state). These changes also enable inputting and deleting items via NgRx actions.

Overall Flow

So with all of the pieces defined, you can do an npm run serve and see the application in action. The flows follow similar patterns, let’s look at the “add item” flow that we discussed in the first sections.

Instead of calling the service method directly in the component, when items are to be added the following occurs:

  1. User enters text for the item to be added
  2. User clicks the “create item” button
  3. The component dispatches a [to-do] add item action
  4. An effect occurs to take the value passed in the [to-do] add item props to be sent to the localStorage via call to the “to-do service”. The effect returns a [to-do] load items action
  5. When the [to-do] load items action is dispatched, it creates a new state in the store and this is all passed to the template via the selectors

This all demonstrates the different parts of the flux pattern in action. To see this visually, I also recommend installing Chrome’s Redux Devtools Extension to see the different events happening.

If you’re having trouble getting it to run, you can also skip ahead to a complete version of the application by doing “git clone — branch after-ngrx https://github.com/andrewevans0102/to-do-with-ngrx.git

Wrapping Up

I hope you enjoyed my article, and encourage you to try using NgRx in your applications today. The flux pattern can seem very complicated at first, but once you’ve walked through adding it to an application it becomes more intuitive.

I also want to point out that the flux pattern is not necessarily the only solution. It is just a solution that scales well, and makes maintenance of applications (especially very large ones) less difficult. There are many apps out there that use other patterns, or something entirely unique. Development of good applications is based on the needs and conditions of the work. I hope with this post you can see the benefit of using flux as one way to design your application.

There are lots of great articles on flux (and NgRx) and I encourage you to check them out. I also recommend checking out the NgRx Getting Started docs for more.