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
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 dispatchesloadMessages
action withchannelId
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 inmessages
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.