Blogs

NgRx in AG04

Theme
Software development

Looking back at the last few months, there hasn’t passed a working day that I didn’t work with NgRx. In the following post, I will write about how we are using NgRx in AGENCY04. The goal of this post isn’t to find out what NgRx is, but rather to share our personal experience while using NgRx.

I’m part of team developing a business application using Angular 6. We can claim that our application performance has been greatly improved by using state management.

Angular logo

I’ll create a simple application and provide a link to my GitHub repository, so you can just clone and play with the code.

Architecture

NgRx architecture

This is a basic schema for NgRx flow in our architecture. We define state inside of every feature module. Every model in diagram (service, effect, reducer etc.) has its own task that it is responsible for. In most cases, everything starts in a smart component, where we are dispatching specific action.

Effect gets triggered by dispatching action, which is due to some effects that needs to be called before reducer. In 90% cases we are calling REST service from effects. Then we are using received data for dispatching new action to save new data to store.

With this new action, that is dispatched in reducer, we are saving new and previous state. When the state is saved in store, we can then subscribe to the property selector in the smart component. After that, we are passing that result to view component through input method.

Example

In these few short sentences I explained how NgRx works in our applications. Now I’ll setup a simple application. First, a project will be create with using Angular CLI:

ng new ngrx-example

Second step is installing NgRx library. Below is a command for installing library:

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

After installation, the NgRx ecosystem is ready to use. Next step is importing StoreModule and EffectsModule in AppModule.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { routerReducer } from '@ngrx/router-store';
import { EffectsModule } from '@ngrx/effects';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { RouterModule } from '@angular/router';
import { metaReducers } from './app.reducer';
import { ClubsModule, mainRoutes } from './clubs/clubs.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    ClubsModule,
    StoreModule.forRoot(
      {
        routerReducer
      },
      {
        metaReducers
      }
    ),
    EffectsModule.forRoot([]),
    StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
    RouterModule.forRoot(mainRoutes, { useHash: true })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Before starting with developing, it’s recommended to setup a meta-reducer for logging every action. Next step is installing ngrx-store-freeze:

npm i --save-dev ngrx-store-freeze

When installation of ngrx-store-freeze is done, it’s necessary to create file app.reducer.ts. Also, it is mandatory to create a debugMetaReducer function that will log old state, action that is dispatched and new state.

import { Action, ActionReducer, MetaReducer } from '@ngrx/store';
import { environment } from 'src/environments/environment';
import { storeFreeze } from 'ngrx-store-freeze';

export function debugMetaReducer(
  reducer: ActionReducer<object>
): ActionReducer<object> {
  return function(oldState: object, action: Action): object {
    const newState = reducer(oldState, action);
    console.groupCollapsed(
      `%c NgRx store update by '${action.type}'`,
      'color: #e2001a'
    );
    console.log('Old state: ', oldState);
    console.log('Action: ', action);
    console.log('New state: ', newState);
    console.groupEnd();

    return newState;
  };
}

export const metaReducers: MetaReducer<any>[] = [debugMetaReducer, storeFreeze];

Meta-reducers

Developers can think of meta-reducers as hooks into the action->reducer pipeline. Meta-reducers allow developers to pre-process actions before normal reducers are invoked.

Application structure

In this simple application, one extra module will be created. That module will have store folder, service, smart component and view component. In store folder will be five files (clubs.actions.ts, clubs.effects.ts, clubs.reducer.ts, clubs.selector.ts and clubs.state.ts).

├── app
 │ ├── app.component.scss
 │ ├── app.component.html
 │ ├── app.component.ts
 │ ├── app.module.ts
 │ ├── app.reducer.ts
 │ ├── clubs
 │    ├── clubs.component.ts
 │    ├── clubs.module.ts
 │    ├── clubs.service.ts
 │    ├── index.ts
 │    ├── components
 │    │    └── table
 │    │        ├── table.component.scss
 │    |        ├── table.component.html
 │    |        └── table.component.ts
 │    └── store
 │         ├── clubs.actions.ts
 │         ├── clubs.effects.ts
 │         ├── clubs.reducer.ts
 │         ├── clubs.selector.ts
 │         └── clubs.state.ts
 ├── assets
 │ ├── dummy
 │ │   └── clubs.json
 │ └── img
 │     ├── asc.png
 │     └── desc.png
 ├── environments
 │ ├── environment.prod.ts
 │ └── environment.ts
 ├── browserslist
 ├── index.html
 ├── main.ts
 ├── polyfills.ts
 ├── styles.css
 ├── test.ts
 ├── tsconfig.app.json
 ├── tsconfig.spec.json
 └── tslint.json

Setting store inside specific module

During creating store, it’s compulsory to create state. The main goal is to develop store in every module that is used. Also, it’s necessary to init module state name (clubs.state.ts) and afterwards import that in ClubsModule.

export const clubStateName = 'club-module';

In the view component there will be a table for displaying names of the clubs, short name, club code, name of stadium, capacity and coach. There will be a nice feature of sorting data per every column. Then, it’s recommended to init some enum that will have two types of sorting (ascending and descending). It will be possible to save name of column and type of sorting in the store.

export const clubStateName = 'club-module';

export const enum SortOrder {
  ASCENDING = 'ASC',
  DESCENDING = 'DESC'
}

export interface ClubsState {
  id: number;
  name: string;
  short_name: string;
  club_code: string;
  stadium: string;
  capacity: number;
  manager: string;
}

export interface SortState {
  field: string;
  order: string;
}

export interface ClubModuleState {
  clubs: ClubsState[];
  sort: SortState;
}

export const initialClubModuleState = {
  clubs: null,
  sort: {
    field: 'name',
    order: SortOrder.ASCENDING
  }
};

Actions

Actions are one of the main building blocks in NgRx. Actions express unique events that happen throughout your application. From user interaction with the page, external interaction through network requests, and direct interaction with device APIs, these and more events are described with actions.

There will be three types of action (request for clubs, respond for clubs and sorting). In action for response and sorting it is needed to define the type of payload. ActionType is used to define the type of action, so it can be referenced in effect and reducer.

import { Action } from '@ngrx/store';
import { ClubsState, SortState } from './clubs.state';

export const enum ClubsActionType {
  REQUEST_CLUBS = '[CLUBS MODULE] REQUEST_CLUBS',
  RESPOND_CLUBS = '[CLUBS MODULE] RESPOND_CLUBS',
  SORT_CLUBS = '[CLUBS MODULE] SORT_CLUBS'
}

export class RequestClubsAction implements Action {
  readonly type: ClubsActionType = ClubsActionType.REQUEST_CLUBS;
}

export class RespondClubsAction implements Action {
  readonly type: ClubsActionType = ClubsActionType.RESPOND_CLUBS;

  constructor(public payload: { clubs: ClubsState[] }) {}
}

export class SortClubsAction implements Action {
  readonly type: ClubsActionType = ClubsActionType.SORT_CLUBS;

  constructor(public payload: { sort: SortState }) {}
}

export type ClubAction =
  | RequestClubsAction
  | RespondClubsAction
  | SortClubsAction;

Reducers

Reducers in NgRx are responsible for handling transitions from one state to the next state in your application. Reducer functions handle these transitions by determining which actions to handle based on the action’s type.

Reducer is triggered by dispatching an action that is going to be called. They connect everything in background anytime a new action is dispatched.

Within this application there are two actions that will update state. First action RESPOND_CLUBS get payload with the clubs from response and then update clubs in state. Second action SORT_CLUBS allows getting the type of sort (field and order).

First thing is to save new sort that is dispatched. After that, we need to copy array before sorting, because the array is frozen in strict mode.

Next step is to check type of sorting, then sorting array of clubs and at the end returning new state. At the end, it is mandatory to copy the old state if there are not changes in any other states. Finally, it’s necessary to set new changes in state. Also, it is needed to set default case in switch statement, where it is possible to return old state.

import {
  ClubModuleState,
  initialClubModuleState,
  ClubsState,
  SortState
} from './clubs.state';
import {
  ClubAction,
  ClubsActionType,
  RespondClubsAction,
  SortClubsAction
} from './clubs.actions';

export function clubReducer(
  oldState: ClubModuleState = initialClubModuleState,
  action: ClubAction
): ClubModuleState {
  switch (action.type) {
    case ClubsActionType.RESPOND_CLUBS: {
      const clubs: ClubsState[] = (action as RespondClubsAction).payload.clubs;

      const newState = {
        ...oldState,
        clubs
      };

      return newState;
    }
    case ClubsActionType.SORT_CLUBS: {
      const sort: SortState = (action as SortClubsAction).payload.sort;
      const clubs: ClubsState[] = oldState.clubs.slice().sort((a, b) => {
        const clubA =
          sort.field !== 'capacity'
            ? a[sort.field].toUpperCase()
            : a[sort.field];
        const clubB =
          sort.field !== 'capacity'
            ? b[sort.field].toUpperCase()
            : b[sort.field];

        if (sort.order === 'ASC') {
          return clubA > clubB ? 1 : clubA < clubB ? -1 : 0;
        } else {
          return clubA < clubB ? 1 : clubA > clubB ? -1 : 0;
        }
      });

      const newState = {
        ...oldState,
        sort,
        clubs
      };

      return newState;
    }
    default:
      return oldState;
  }
}

Effects

Effects are an RxJS powered side effect model for Store. Effects use streams to provide new sources of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events.

Before starting with effects, in 90% of cases it is necessary to have REST service, that will return some response from backend (in our case we have dummy data in JSON file and it will return list of clubs).

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { ClubsState } from './store/clubs.state';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ClubsService {
  constructor(private http: HttpClient) {}

  getClubs(): Observable<ClubsState[]> {
    return this.http
      .get<ClubsState[]>('./assets/dummy/clubs.json', {
        observe: 'response'
      })
      .pipe(map(res => res.body['clubs']));
  }
}
view raw

Effect is triggered when specific action is dispatched. It is similar to what reducers are doing, but in 90% of cases effects is used to calling REST service. After the effect is done, it returns new action that is going to dispatch and change/save data in store.

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ClubsService } from '../clubs.service';
import {
  RequestClubsAction,
  ClubsActionType,
  RespondClubsAction
} from './clubs.actions';
import { ClubsState } from './clubs.state';

@Injectable()
export class ClubsEffects {
  @Effect()
  requestClubs$: Observable<Action> = this.actions$.pipe(
    ofType<RequestClubsAction>(ClubsActionType.REQUEST_CLUBS),
    switchMap(action => {
      return this.clubService.getClubs().pipe(
        map((response: ClubsState[]) => {
          return new RespondClubsAction({ clubs: response });
        })
      );
    })
  );

  constructor(
    private readonly actions$: Actions,
    private readonly clubService: ClubsService
  ) {}
}

Selector

Selectors are pure functions used for obtaining slices of store state. @ngrx/store provides a few helper functions for optimizing this selection. Selectors provide many features when selecting slices of state.

After all of the above is done (state, actions, reducer and effect), it’s obligatory to setup the selector. Selector is used to get specific data that is needed in specific view component. First step is to setup which state name is used. Second step is to export const with specific data that returns Observable with property type.

import { createFeatureSelector, Selector, createSelector } from '@ngrx/store';
import {
  ClubModuleState,
  clubStateName,
  ClubsState,
  SortState
} from './clubs.state';

const selectClubModule = createFeatureSelector<ClubModuleState>(clubStateName);

export const selectClub: Selector<object, ClubsState[]> = createSelector(
  selectClubModule,
  (state: ClubModuleState) => state.clubs
);

export const selectSortState: Selector<object, SortState> = createSelector(
  selectClubModule,
  (state: ClubModuleState) => state.sort
);

Smart and view component

Smart component is component where all actions are dispatched and where is possible to get all data from state using selectors. Input and output method is used to pass data or dispatch some function to/from view component. In OnInit method is allowed to dispatch actions that are needed. Also, it’s available to select properly selector for input method.

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { selectClub, selectSortState } from './store/clubs.selector';
import { RequestClubsAction, SortClubsAction } from './store/clubs.actions';
import { Observable } from 'rxjs';
import { ClubsState, SortState } from './store/clubs.state';

@Component({
  selector: 'app-clubs',
  template: `
    <app-table
      [clubs]="clubs$ | async"
      [sortState]="sortState$ | async"
      (doSort)="sort($event)"
    ></app-table>
  `
})
export class ClubsComponent implements OnInit {
  clubs$: Observable<ClubsState[]>;
  sortState$: Observable<SortState>;

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

  ngOnInit() {
    this.store.dispatch(new RequestClubsAction());

    this.clubs$ = this.store.pipe(select(selectClub));
    this.sortState$ = this.store.pipe(select(selectSortState));
  }

  sort(event): void {
    this.store.dispatch(new SortClubsAction({ sort: event }));
  }
}

In the smart component, the async pipe is used for automatically subscribe to Observable and then return a new emit value. In case when changes happen in the state, async pipe automatically makes proper changes in component. Also, when the component is destroyed, async pipe automatically unsubscribes to avoid memory leak.

View component takes care for displaying the data and triggering event. It is not aware of how the data is retrieved or how the state changes. View components are based on the value of their input properties, nothing else. In view component folder const is initialised with the names and values for each column in table. Data is sent from smart component through input method. Output method for sorting is emitting new value that needs to be dispatched.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ClubsState, SortState, SortOrder } from '../../store/clubs.state';
import { TableSettings, tableSettings } from './table.settings';

@Component({
  selector: 'app-table',
  templateUrl: './table.component.html',
  styleUrls: ['./table.component.scss']
})
export class TableComponent {
  @Input() clubs: ClubsState[];
  @Input() sortState: SortState;
  @Output() doSort = new EventEmitter<SortState>();

  tableSettings: TableSettings[] = tableSettings;

  constructor() {}

  sort(field: string): void {
    const order: string =
      field === this.sortState.field
        ? SortOrder.ASCENDING === this.sortState.order
          ? SortOrder.DESCENDING
          : SortOrder.ASCENDING
        : SortOrder.ASCENDING;

    this.doSort.emit({ field, order });
  }
}
<table class="table">
  <thead>
    <tr>
      <th
        scope="col"
        *ngFor="let settings of tableSettings"
        (click)="sort(settings.value)"
      >
        {{ settings.name }}

        <img
          src="./assets/img/{{
            sortState.order === 'ASC' ? 'asc' : 'desc'
          }}.png"
          *ngIf="sortState.field === settings.value"
          alt="{{ sortState.order }}"
        />
      </th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let club of clubs">
      <td *ngFor="let settings of tableSettings">{{ club[settings.value] }}</td>
    </tr>
  </tbody>
</table>
export interface TableSettings {
  name: string;
  value: string;
}

export const tableSettings: TableSettings[] = [
  {
    name: 'Name',
    value: 'name'
  },
  {
    name: 'Short name',
    value: 'short_name'
  },
  {
    name: 'Club code',
    value: 'club_code'
  },
  {
    name: 'Stadium',
    value: 'stadium'
  },
  {
    name: 'Capacity',
    value: 'capacity'
  },
  {
    name: 'Manager',
    value: 'manager'
  }
];
view raw

Conclusions

In this article I tried to explain how and in which way we use NgRx in AG04. NgRx is very powerful and stable library that is very useful in big enterprise applications. This is an example how you can setup architecture for your application. However, if you’re going to work with this type of approach, it’ll take a lot more code to type.

GitHub repo

Next

Blog

Dagger 2 — Dependency Injection basics in Android

Company

Our people really love it here

How it all started

Est. in 2014., gathering eight employees with eyes set on the future. No matter how set they were, they couldn’t predict the success and extent of growth that would ensue. Today there are more than 100 of us, and people are here to stay.

Stability in unstable times

The turmoil of 2020 caused great inconvenience for people all over the world. However, this did not affect our business. Quite the opposite — we not only kept all jobs and salaries intact, but we also grew in size. And we keep expanding. 

Contact

We’d love to hear from you