How to use Redux in an Angular application
To use Redux in the Angular framework, we can use the NgRx library. This is a reactive state management library. With NgRx, we can get all events (data) from the Angular app and put them all in the same place (Store). When we want to use the stored data, we need to get it (dispatch) from the store using the RxJS library. RxJS (Reactive Extensions for JavaScript) is a library based on the Observable pattern that is used in Angular to process asynchronous operations.
We can also use ViewChild for nested components. But in the case of a large project, these solutions will increase the project complexity. If we have a large number of components, we risk losing control over the data flow within a component (where did this data come from and what is its intended destination?)
This is the reason why we use Redux in Angular: the store and the unidirectional data flow reduce the complexity of the application. The flow is more clear and easy to understand for new team members.
3. Implementation
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { TodosModule } from './modules/todos/todos.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, TodosModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
imports: [ CommonModule, HttpClientModule, NgbModule, FormsModule, ReactiveFormsModule, NgxPaginationModule, StoreModule.forRoot({}) ], providers: [TodosService]
- type: This is a read-only string that describes what the action stands for. For example GET_TODO.
- payload: The type of this property depends on what type of data this action needs to send to the reducer. In the previous example, the payload will be a string containing a todo. Not all actions need to have a payload.
import { Action } from '@ngrx/store'; import { Todo } from '../../models/todo'; export enum TodosActionType { GET_TODOS = 'GET_TODOS', GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS', GET_TODOS_FAILED = 'GET_TODOS_FAILED' } export class GetTodos implements Action { readonly type = TodosActionType.GET_TODOS; } export class GetTodosSuccess implements Action { readonly type = TodosActionType.GET_TODOS_SUCCESS; constructor(public payload: Array<Todo>) { } } export class GetTodosFailed implements Action { readonly type = TodosActionType.GET_TODOS_FAILED; constructor(public payload: string) { } } export type TodosActions = GetTodos | GetTodosSuccess | GetTodosFailed;
import { TodosActions, TodosActionType } from './todos.actions'; import { Todo } from '../../models/todo'; export const initialState = {}; export function todosReducer(state = initialState, action: TodosActions) { switch (action.type) { case TodosActionType.GET_TODOS: { return { ...state }; } case TodosActionType.GET_TODOS_SUCCESS: { let msgText = ''; let bgClass = ''; if (action.payload.length < 1) { msgText = 'No data found'; bgClass = 'bg-danger'; } else { msgText = 'Loading data'; bgClass = 'bg-info'; } return { ...state, todoList: action.payload, message: msgText, infoClass: bgClass }; } case TodosActionType.GET_TODOS_FAILED: { return { ...state }; } }
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { Todo } from './../../models/todo'; import { headers } from '../../headers/headers'; @Injectable({ providedIn: 'root' }) export class TodosService { baseUrl: string; constructor(private http: HttpClient) { this.baseUrl = 'http://localhost:3000'; } getAPITodos() { return this.http.get(`${this.baseUrl}/todos`, { headers }) .pipe(catchError((error: any) => throwError(error.message))); } }
import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { TodosService } from './../../services/todos/todos.service'; import { TodosActionType, GetTodosSuccess, GetTodosFailed, AddTodoSuccess, AddTodoFailed, UpdateTodoSuccess, UpdateTodoFailed, DeleteTodoSuccess, DeleteTodoFailed } from './todos.actions'; import { switchMap, catchError, map } from 'rxjs/operators'; import { of } from 'rxjs'; import { Todo } from '../../models/todo'; @Injectable() export class TodosEffects { constructor( private actions$: Actions, private todosService: TodosService ) { } @Effect() getTodos$ = this.actions$.pipe( ofType(TodosActionType.GET_TODOS), switchMap(() => this.todosService.getAPITodos().pipe( map((todos: Array<Todo>) => new GetTodosSuccess(todos)), catchError(error => of(new GetTodosFailed(error))) ) ) ); }
import { Component, OnInit } from '@angular/core'; import { Todo } from './../../common/models/todo'; import { Store } from '@ngrx/store'; import * as Todos from '../../common/store/todos/todos.actions'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.scss'] }) export class TodoListComponent implements OnInit { todos: Array<Todo>; message: string; bgClass: string; p = 1; constructor(private store: Store<any>) { } ngOnInit() { this.store.dispatch(new Todos.GetTodos()); this.store.select('todos').subscribe(response => { this.todos = response.todoList; this.message = response.message; this.bgClass = response.infoClass; setTimeout(() => { this.message = ''; }, 2000); }, error => { console.log(error); }); } }
<div class="container-fluid" *ngIf="todos"> <div class="row"> <div class="col-12"> <div class="card mt-5"> <div class="card-header"> <h1 class="display-6 d-inline">Todo App</h1> <app-add-todo></app-add-todo> </div> <div class="card-body"> <table class="table"> <tbody> <tr *ngFor="let todo of todos | paginate: { itemsPerPage: 10, currentPage: p }"> <td> <code>{{todo | json}}</code> </td> </tr> </tbody> </table> <pagination-controls (pageChange)="p = $event"></pagination-controls> </div> </div> </div> </div> </div>
imports: [ CommonModule, HttpClientModule, NgbModule, FormsModule, ReactiveFormsModule, NgxPaginationModule, StoreModule.forRoot({}), StoreModule.forFeature('todos', todosReducer, { metaReducers }), EffectsModule.forRoot([]), EffectsModule.forFeature([TodosEffects]) ],
4. Conclusions
- The NgRx library is awesome if we want to use it in larger applications (as seen in the application we presented in this article, we needed to do some configurations, but if you have more than 20-30 components, this library will be helpful).
- A large application that uses the NgRx library will be easy to understand for a new team member.
- It is easy to follow the data flow and to debug the application.
- By using Redux in an Angular application, the NgRx library becomes more robust and flexible.