Introduction to NgRx

thisdotmedia_staff

This Dot Media

Posted on March 1, 2021

Introduction to NgRx

Introduction to NgRx

Why

Angular projects are getting more and more complex these days. With handling all the user interaction, application state and accessing this state in every place, it might be necessary but quickly become over complicated. This is why having a one, global app-wide state management system that can be accessible throughout the application may be very useful in modern frontend application. One of the solutions available for Angular applications is NgRx.

What

In this post, I will give you a quick introduction on how to get started using NgRx in you Angular application. I'll base this intro on a simple Angular app where we can display a photo which then can be liked or disliked. You can find the entry point for this application on my GitHub repo. If you want to follow this article's code, please clone the repository and checkout entry_point tag.

git clone git@github.com:ktrz/introduction-to-ngrx.git

git checkout entry_point
Enter fullscreen mode Exit fullscreen mode

After cloning just install all the dependencies

yarn install
Enter fullscreen mode Exit fullscreen mode

and you can see the example app by running

yarn start -o
Enter fullscreen mode Exit fullscreen mode

We will convert this application to use the NgRx state management in just few steps.

How

Key concepts

Let's start with introducing a few key concepts and building blocks of NgRx. NgRx is a state management tool inspired by Redux, so the building blocks might be familiar to Redux users. To build and operate on our state, we will need the following basic building blocks:

  • Store is where state of the application will be stored. It's both an Observable of a state and an Observer of action.
  • Actions describe all the possible and unique events that can occur within the application. They can occur ie. by user interaction, communication with the server or can be a result of other actions.
  • Reducers are what binds the actions and the state. All the state changes must occur as result of an action. Those changes are handled by pure functions called reducers. They take the current state and the latest action and compute new state value based on that.
  • Selectors - to retrieve part of the state that we're interested in, we use pure functions which extract the portion of the state that a given component might be interested in.

NgRx flow

NgRx makes sure that all the interactions in the application follow a certain unidirectional flow. The following diagram illustrates the general flow of the state:NgRx basic flow

Getting started

The easiest way to add NgRx to the project and get started is to use a Angular CLI schematic.

sh
ng add @ngrx/store@latest

This command will

  • add the @ngrx/store package to package.json -> dependencies
  • yarn install or npm install those dependencies
  • import the StoreModule.forRoot({}, {}) in your AppModule

Define Actions

The first thing to do after installing the necessary dependencies is to define some actions that can occur in our application. In our case, this can define the following action:

  • like photo
  • dislike photo

To put that into code, let's create the following file: src/app/store/photo.actions.ts

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

export const likePhoto = createAction('[Photo List] Like Photo');
export const dislikePhoto = createAction('[Photo List] Dislike Photo');
Enter fullscreen mode Exit fullscreen mode

In this file, we define all the actions (action creators) that can occur in our app. In general, the shape of the action has to be in the following share:

const someAction = {
  type: 'Unique type',
  /* ... some additional properties */
}
Enter fullscreen mode Exit fullscreen mode

The required part is the type property which will be used to identify our action. Other properties are optional and might be used to pass some additional data associated with the event ie. id of the liked/disliked photo.

What we've just defined are action creators. They are functions which return an action of a specific type. To create ie. like action we can do it as following:

const likeAction = likePhoto()
Enter fullscreen mode Exit fullscreen mode

Define Reducer

Now that we have our actions defined, we need a way to consume them and thus make actual changes to the store's state. As described before, this is done using pure functions called reducers. We could define the functions from scratch using ie. switch case statements, but NgRx comes with some handy helper functions which make this process much nicer. Let's create a file to keep our photo reducer in: src/app/store/photo.reducer.ts

import {createReducer, on} from '@ngrx/store';
import {dislikePhoto, likePhoto} from './photo.actions';

export type PhotoState = number;

const initialState: PhotoState = 0;

export const photoReducer = createReducer(
  initialState,
  on(likePhoto, state => state + 1),
  on(dislikePhoto, state => state - 1)
);
Enter fullscreen mode Exit fullscreen mode

As you can see, you import the actions which we've defined previously and use them in our reducer. createReducer is an utility function which creates a reducer with a provided initial state. This gives us a nice quick way to define new reducers as well as great type inference. TypeScript knows the shape of state that photoReducer operates on from the shape of initialState object.

on(...) function can be considered as case statements within a switch. We define that if we encounter a given action, we produce a new state derived from its previous value and optional properties provided within the action.

All this code could be rewritten in the following shape. In my opinion, the construction mentioned above is both more consise and self explanatory as well, so I suggest using it.

export function photoReducer(state = initialState, action: Action): PhotoState {
  switch (action.type) {
    case likePhoto.type:
      return state + 1;
    case dislikePhoto.type:
      return state - 1;
    default:
        return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

After creating a reducer, we need to let our application know that we want to use it. We can define it in our AppModule file and add it to the properties of the StoreModule.forRoot's first param.

  imports: [
    /* ... other modules */
    StoreModule.forRoot({
        photo: photoReducer
      },
      {}
    )
  ],
Enter fullscreen mode Exit fullscreen mode

Define Selectors

Now that we've defined both possible actions and reducer to handle them, we need a way to get the data from the store. To do this, we'll create another pure function called selectors. The responsibility of selectors is to transform the whole state object (which can we a really large object in real life examples) into small bits necessary for a specific part of the application. In our case, we need a way to get the photo information along with the number of likes/dislikes that it currently has.

Let's create another file in our src/app/store directory src/app/store/photo.selectors.ts

import {createSelector} from '@ngrx/store';
import {PhotoState} from './photo.reducer';

const selectPhotoFeature = (state: { photo: PhotoState }) => state.photo;

export const selectPhoto = createSelector(selectPhotoFeature, likes => ({
    title: 'Introduction to NgRx',
    url: 'https://ngrx.io/assets/images/ngrx-badge.png',
    likes
  })
);
Enter fullscreen mode Exit fullscreen mode

We are again using the utility function provided with NgRx - createSelector. Because our state can be a nested tree of objects, this enables us to create more complex selectors based on the already existing ones. In this case, we combine selecting a photo slice of our whole state with selection of the whole photo object (in this case most of its props are static). Whenever a number of likes change, we will get a new instance of the photo, which makes detection of changes super easy and performant.

Use all the building blocks in our component

Now that we've defined all the necessary building blocks, we can finally start using them in our AppComponent. The first thing we need to do is inject the Store service into our component.

import {Store} from '@ngrx/store';

interface AppState {
  photo: PhotoState;
}

@Component({/* ... */})
export class AppComponent {
  constructor(private store: Store<AppState>) {
  }
}
Enter fullscreen mode Exit fullscreen mode

This gives us access to both selecting data from the store (as an Observable) and notifying it about actions happening (by dispatching actions).

Now let's select the state of the photo into a component's property and define methods for dispatching like and dislike actions.

import {select, Store} from '@ngrx/store';
import {selectPhoto} from './store/photo.selectors';
import {dislikePhoto, likePhoto} from './store/photo.actions';

@Component({/* ... */})
export class AppComponent {
  photo$ = this.store.pipe(select(selectPhoto));

  constructor(private store: Store<AppState>) {
  }

  onLike(): void {
    this.store.dispatch(likePhoto());
  }

  onDislike(): void {
    this.store.dispatch(dislikePhoto());
  }
}
Enter fullscreen mode Exit fullscreen mode

To select data from store, we can basically treat it as an Observable and use pipe operator on it. To select data with our previously created selectors, we can use select operator provided by @ngrx/store package.

To dispatch actions and notify the store of it, we can use dispatch() method and feed it with the necessary action (also defined above).

Now all that is left to do is hook up all this state and methods into our component's template.

@Component({
  selector: 'app-root',
  template: `
    <div class="photos">
      <app-photo class="photo" [photo]="photo$ | async" (like)="onLike()" (dislike)="onDislike()"></app-photo>
    </div>
  `,
})
Enter fullscreen mode Exit fullscreen mode

As you can see, we use async pipe to unwrap the photo$ observable and pass a plain object into the [photo] input. The app-photo component provides two outputs: like and dislike which we can react to with our AppComponent's methods.

For clarity purposes, let's define our PhotoComponent in src/app/photo/photo.component.ts (It's already defined within the entry point repo but needs some minor tweaks.)

import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Photo} from './photo';

@Component({
  selector: 'app-photo',
  template: `
    <mat-card class="example-card" *ngIf="photo">
      <mat-card-header>
        <div mat-card-avatar class="example-header-image"></div>
        <mat-card-title>{{photo.title}}</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <img mat-card-image [src]="photo.url" alt="Intro to NgRx">
      </mat-card-content>
      <mat-card-actions align="end">
        <button mat-icon-button color="warn" (click)="onLike()">
          <ng-container *ngIf="photo.likes > 0; else notLikedButton">
            <mat-icon [matBadge]="photo.likes" matBadgeColor="warn" color="primary">thumb_up</mat-icon>
          </ng-container>
          <ng-template #notLikedButton>
            <mat-icon color="primary">thumb_up_off_alt</mat-icon>
          </ng-template>
        </button>
        <button mat-icon-button (click)="onDislike()">
          <ng-container *ngIf="photo.likes < 0; else notDislikedButton">
            <mat-icon [matBadge]="-photo.likes" matBadgeColor="warn" color="primary">thumb_down</mat-icon>
          </ng-container>
          <ng-template #notDislikedButton>
            <mat-icon color="primary">thumb_down_off_alt</mat-icon>
          </ng-template>
        </button>
      </mat-card-actions>
    </mat-card>

  `,
  styleUrls: ['./photo.component.scss']
})
export class PhotoComponent {
  @Input() photo?: Photo | null;

  @Output() like = new EventEmitter();
  @Output() dislike = new EventEmitter();

  onLike(): void {
    this.like.next();
  }

  onDislike(): void {
    this.dislike.next();
  }
}
Enter fullscreen mode Exit fullscreen mode

and PhotoModule in src/app/photo/photo.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {MatBadgeModule} from '@angular/material/badge';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import { PhotoComponent } from './photo.component';

@NgModule({
  declarations: [PhotoComponent],
  imports: [
    CommonModule,
    MatCardModule,
    MatButtonModule,
    MatIconModule,
    MatBadgeModule
  ],
  exports: [PhotoComponent]
})
export class PhotoModule { }
Enter fullscreen mode Exit fullscreen mode

Now start your application again and see the result it the browser. You can still like/dislike the photo and the state of the app is stored withing NgRx store.

Conclusion

As you can see, setting up this example required a bit more code to start with. But when the application grows, this additional code pays off as we get one central place where all the state changes are happening. We can see exactly what actions are being dispatched (ie. by using Redux Devtools) and therefore our application is more maintainable.

In case you have any questions you can always tweet or DM at me @ktrz. I'm always happy to help!

More articles are coming soon! Do not miss any of them and visit the This Dot Blog to be up-to-date.


This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit thisdotlabs.com.

This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.

💖 💪 🙅 🚩
thisdotmedia_staff
This Dot Media

Posted on March 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Introduction to NgRx
angulartypescriptngr Introduction to NgRx

March 1, 2021