Today we are going to learn how to introduce on using REDUX State management in an Angular Web Application with the NGRX library

REDUX equivalent for Angular is NGRX.

We will see each building blocks of NGRX step by step

NgRx State Management Lifecycle Diagram
Source: https://ngrx.io/

Store

The Store is a controlled state container designed to help write performant and escalable applications on top of Angular.

Effects

The Effects use streams to provide new sources of actions. To modify state based on external interactions such as network requests, web socket messages and time-based events. So they are the prefered method to fetch data and to keep some of the application logic.

Selectors

The Selectors are pure functions used for obtaining slices of store state. So they select pieces of information from the Store be be consumed by other parts of the application logic.

Reducers

The 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. In other words, they receive actions with a data payload and the change the state storing the new data as desired.

After understanding basic building blocks of NGRX State Management, now we are going to build an example small chat app with NGRX State Management.

Since, we already have created a chat app earlier In previous post here . As a result, we are going to introduce the NGRX State management in it for updating the UI while SENDING / RECEIVING the message.

1. Setup the Project

/** Clone Repo */
git clone https://github.com/unimedia-technology/amplify-chat-angular.git

/** Enter into project directory */
cd amplify-chat-angular

/** Install the dependencies */
npm i

/** Create a new git branch */
git checkout ngrx

2. Install NGRX Dependencies

ng add @ngrx/store @ngrx/effects @ngrx/component

3. Create Actions

Here, We will create 4 actions that are necessary to manage the state of the chat app.

File: store/actions/actions.ts

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

export const loadMessages = createAction('[Chat] Load Messages', props<{ channelId: string }>());
export const loadMessagesSuccess = createAction('[Chat] Load Messages Success', props<{ messages: any[] }>());

export const sendMessage = createAction('[Chat] Send Message', props<{ message }>());
export const addMessageToList = createAction('[Chat] Add Message To List', props<{ message }>());

4. Create Effects

Effects provide a way to interact with those services and isolate them from the components.

Usage

Effects are useful, If you want to handle tasks such as fetching data, long-running tasks that produce multiple events, and other external interactions.

Note: To remember, In most scenarios, it will dispatch action(s), But it is not compulsory to always dispatch action(s).

To write effects without dispatching the action(s), pass 2nd parameter to createEffect() functions with { dispatch: false }

File: state/effects/effects.ts

import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map, switchMap } from 'rxjs/operators';
import * as chatActions from '../actions/actions';
import { from } from 'rxjs';
import { APIService } from '../../API.service';

@Injectable()
export class ChatEffects {

  constructor(
    private actions$: Actions,
    private api: APIService,
  ) { }

  /** Load the List of Messages */
  loadMessages$ = createEffect(() => this.actions$.pipe(
    ofType(chatActions.loadMessages),
    switchMap(({ channelId }) => {
      return from(this.api.MessagesByChannelId(channelId)).pipe(
        map((res: any) => {
          return chatActions.loadMessagesSuccess({ messages: res.items });
        }),
      );
    })
  ));

  /** Send message and call no actions */
  sendMessage$ = createEffect(() => this.actions$.pipe(
    ofType(chatActions.sendMessage),
    switchMap(({ message }) => {
      return from(this.api.CreateMessage(message));
    })
  ), { dispatch: false });
}

5. Create Reducers

Each reducer function takes the latest Action dispatched, the current state, and determines whether to return a newly modified state or the original state. Now, in the following example we’ll guide you on how to write reducer functions, register them in your Store, and compose feature states.

File: store/reducers/reducers.ts

import { Action, createReducer, on } from '@ngrx/store';
import * as chatActions from '../actions/actions';

export interface IChatState {
  messages: any[];
}

/** Initial State */
export const initialState: IChatState = {
  messages: [],
};

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

const reducer = createReducer<IChatState>(
  initialState,

  /** Loaded Messafes */
  on(chatActions.loadMessagesSuccess, (state, { messages }) => ({
    ...state,
    messages
  })),

  /** Add message to the messages array */
  on(chatActions.addMessageToList, (state, { message }) => ({
    ...state,
    messages: [...state.messages, message]
  })),
);

6. Create Selectors

The selector that we have created here will return the observable of all the messages

File: store/selectors/selectors.ts

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

export const selectChatState = (state) => state;

export const selectMessages = createSelector(
  selectChatState,
  (state) => state.chat.messages
);

7. Manage the State

In this section, We are going to see which are all the events where we need to update the state:

  • Loading the app
  • Sending a message
  • Receiving a message

File: app.component.ts

import { Component, OnInit } from '@angular/core';
import { NavigationEnd, Router, RouterEvent } from '@angular/router';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';
import { APIService } from './API.service';
import { addMessageToList, loadMessages, sendMessage } from './store/actions/actions';
import { IChatState } from './store/reducers/reducer';
import { selectMessages } from './store/selectors/selectors';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'amplify-chat-angular';
  username: string;
  messages: Observable<any[]>;

  constructor(
    private api: APIService,
    private router: Router,
    private store: Store<IChatState>
  ) { }

  ngOnInit(): void {
    this.router.events.subscribe((events: RouterEvent) => {
      if (events instanceof NavigationEnd) {
        const qParams = this.router.routerState.snapshot.root.queryParams;
        if (qParams && qParams.user) {
          this.username = qParams.user;
        } else {
          this.router.navigate(['/'], { queryParams: { user: 'Dave' } });
        }
      }
    });

    this.listMessages();
    this.onCreateMessage();
  }

  send(event, inputElement: HTMLInputElement): void {
    event.preventDefault();
    event.stopPropagation();
    const input = {
      channelID: '2',
      author: this.username.trim(),
      body: inputElement.value.trim()
    };

    this.store.dispatch(sendMessage({ message: input }));
    inputElement.value = '';
  }

  listMessages(): void {
    this.store.dispatch(loadMessages({ channelId: '2' }));
    this.messages = this.store.pipe(
      select(selectMessages),
      delay(10)
    );
  }

  onCreateMessage(): void {
    this.api.OnCreateMessageListener.subscribe(
      {
        next: (val: any) => {
          console.log(val);
          this.store.dispatch(addMessageToList({ message: val.value.data.onCreateMessage }));
        }
      }
    );
  }
}

Explanation

  • listMessages method dispatches loadMessages action with channelId to fetch all the messages.
  • When user sends a message, sendMessage action is called, and it sends the message.
  • When user receives a message, addMessageToList action is dispatched and it adds the message in messages list

8. Create Template

In this section, We are going to use, ngrxPush  pipe from @ngrx/component.

The ngrxPush pipe serves as a drop-in replacement for the async pipe.

ngrxPush  contains intelligent handling of change detection which will enable us running in zone-full as well as zone-less mode without any changes to the code.

Usage:

The ngrxPush pipe is provided through the ReactiveComponentModule. Therefore to use it add the ReactiveComponentModule to the imports of your NgModule.

File: app.component.html

<div id="root">
  <div class="container">
    <div class="messages">
      <div class="messages-scroller">
        <ng-container *ngIf="messages$ | async as messages">
          <ng-container *ngFor="let message of messages">
            <div [ngClass]="message.author === username ? 'message me' : 'message'">
              {{message.body}}
            </div>
          </ng-container>
        </ng-container>
      </div>

    </div>
    <div class="chat-bar">
      <div class="form">
        <input #messageInput type="text" name="messageBody" placeholder="Type your message here" value=""
          (keyup.enter)="send($event, messageInput)" />
      </div>
    </div>
  </div>
</div>

That’s it, We are now managing the state using NGRX.

Did you enjoy reading this and would like to learn more about angular and redux?, here is another great article, check it out!

Unimedia Technology

Here at Unimedia Technology we have a team of Angular Developers that can develop your most challenging Web Dashboards and Web apps.

RSS
Follow by Email
LinkedIn
Share