Angular Journey: Part 5 - RxJS Touch

Zaki Mohammed Zaki Mohammed
Dec 26, 2024 | 7 min read | 46 Views | Comments

Let us give RxJS touch to our Notesy App which we have created in our Angular journey so far. In this article, we will explore some of the capabilities of RxJS operators and will understand some idealism to follow while dealing with async operations.

Our Notesy App is working like a charm, but it has some areas of improvement from RxJS POV. A RxJS provides a Reactive programming approach as compared to imperative programming. We will transform the Notesy app into a Reactive app by exploring and using concepts related RxJS and its operators. We will also follow some best practices to deal with async operations. So, buckle up as will encounter many new concepts if you are considering yourself a beginner to Angular. We will take the same app which we left in the previous article Angular Journey: Part 4 - Working with Web API.

We will head in this direction:

  • Maintaining State Service
    1. Create Note API Service
    2. Use RxJS Operators for Subscription Logic
    3. Handle Exception Scenario
    4. Redefine Loader Logic
  • Using Reactive State
    1. Create Reactive State
    2. Consume Reactive State
  • Always Unsubscribe
  • State Management (Future Scope)

Maintaining State Service

In this section we will focus on improving our Note Service code logic which is acting as a state service. In Angular, one of the uses of service is to maintain state of the app, in our case it's the Note Service.

1. Create Note API Service

Before we jump into the exploration of RxJS concepts, let us first segregate our HTTP based code logic and the shared Note state variable present in the note service. This is logical step to follow the principal SOC (Separation of Concerns). For this we will create a NoteApiService inside new ""http" folder ("\src\app\http"). Run the below command to create a new service inside "http" folder:

ng g s note-api

Move the API code logic to the newly created service:

import { Injectable } from '@angular/core';
import { Note } from '../models/note.model';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root',
})
export class NoteApiService {
  apiUrl = environment.apiUrl + 'notes';

  constructor(private http: HttpClient) {}

  getNotes() {
    return this.http.get<Note[]>(this.apiUrl);
  }

  addNote(note: Note) {
    return this.http.post<Note>(this.apiUrl, note);
  }

  deleteNoteById(id: string) {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

Update the NoteService and remove the HttpClient dependency, instead use the NoteApiService:

import { Injectable } from '@angular/core';
import { Note } from '../models/note.model';
import { NoteApiService } from '../http/note-api.service';

@Injectable({
  providedIn: 'root',
})
export class NoteService {
  notes: Note[] = [];

  constructor(private noteApiService: NoteApiService) {}

  getNotes() {
    return this.noteApiService.getNotes();
  }

  addNote(note: Note) {
    return this.noteApiService.addNote(note);
  }

  deleteNoteById(id: string) {
    return this.noteApiService.deleteNoteById(id);
  }
}

Run the app to verify if the changes done so far is working fine or not before moving ahead.

2. Use RxJS Operators for Subscription Logic

Move the subscription code logic from the components to the Note Service, this will provide us a way to handle the state changes at service level instead of handling it at individual components. Modify the Note Service code logic as below:

import { Injectable } from '@angular/core';
import { Note } from '../models/note.model';
import { NoteApiService } from '../http/note-api.service';
import { LoaderService } from './loader.service';
import { tap, finalize } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class NoteService {
  notes: Note[] = [];

  constructor(
    private noteApiService: NoteApiService,
    private loaderService: LoaderService
  ) {}

  getNotes(): Observable<Note[]> {
    this.loaderService.show();
    return this.noteApiService.getNotes().pipe(
      tap((notes) => (this.notes = notes)),
      finalize(() => this.loaderService.hide())
    );
  }

  addNote(note: Note): Observable<Note> {
    this.loaderService.show();
    return this.noteApiService.addNote(note).pipe(
      tap((newNote) => this.notes.push(newNote)),
      finalize(() => this.loaderService.hide())
    );
  }

  deleteNoteById(id: string): Observable<void> {
    this.loaderService.show();
    return this.noteApiService.deleteNoteById(id).pipe(
      tap(() => this.notes = this.notes.filter((n) => n.id !== id)),
      finalize(() => this.loaderService.hide())
    );
  }
}

Here, we have moved the entire code logic of next, and complete property of the subscribe method present at component level (list and form components). For this we are using the tap and finalize operators.

The tap operator as the name suggests keep a tap on the next value of the Observable, when it received it allows you to perform any operation, in our case we are modifying the notes state variable.

The finalize operator triggered in error or non-error cases and we are handling the common operation such as hiding the loader post success or failure case.

These operators are used within the pipe method parameter area. We are manually calling the loader's show method before invoking the actual API calls. Now, modify the list and form component code logic where the Note Service methods been used.

Below shows the list component updated code (we are just showing the snippet of the updates):

@Component({...})
export class ListComponent implements OnInit {
  ngOnInit() {
    this.noteService.getNotes().subscribe();
  }

  onRemove(note: Note) {
    this.noteService.deleteNoteById(note.id).subscribe();
  }
}

Below shows the form component updated code (we are just showing the snippet of the updates):

@Component({...})
export class FormComponent {
  onAdd(event: Event) {
    this.noteService
      .addNote(newNote)
      .pipe((this.content = ''))
      .subscribe();
  }
}

3. Handle Exception Scenario

In order to handle the exception cases at Note Service level, we need to add these 3 steps to the Note Service methods:

// 1. import catchError
import { catchError } from 'rxjs';

// 2. create handleError method
handleError(error: any) {
  alert(`Error Occurred: ${error.message}`);
  return of(error);
}

// 3. add catchError operator to the pipe method
catchError(this.handleError),

Let us apply the mentioned changes to the Note Service:

import { tap, finalize, of, catchError } from 'rxjs';

@Injectable({...})
export class NoteService {
  getNotes(): Observable<Note[]> {
    this.loaderService.show();
    return this.noteApiService.getNotes().pipe(
      tap((notes) => (this.notes = notes)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide())
    );
  }

  addNote(note: Note): Observable<Note> {
    this.loaderService.show();
    return this.noteApiService.addNote(note).pipe(
      tap((newNote) => this.notes.push(newNote)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide())
    );
  }

  deleteNoteById(id: string): Observable<void> {
    this.loaderService.show();
    return this.noteApiService.deleteNoteById(id).pipe(
      tap(() => this.notes = this.notes.filter((n) => n.id !== id)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide())
    );
  }
  
  handleError(error: any) {
    alert(`Error Occurred: ${error.message}`);
    return of(error);
  }
}

Here, we have added the catchError operator post tap operator and the handleError method is added at the very end. The catchError get triggered when failure faced by the Observable. Inside the handleError method for now we are just alerting the user regarding the failure, followed by returning the received error back to the call chain.

Here, we are using another opearoter called "of" operator which makes any object to an Observable, we needed this operator because the catchError needs to return and Observable type.

4. Redefine Loader Logic

Will zoom our focus back to the loader code logic and try to improvise it a bit. Currently, the loader is shown or hide based on the methods of the loader service which carries a boolean. In case, where there are multiple calls happening parallelly such code logic will create a loader mess as it will show or hide again and again for each API calls.

To simplify this we are just modifying the logic of loader service, instead of keeping a boolean for indication of the loader, will have an array of string which will keep on adding whenever a new show method is called with given loading value and removed when hide method is called with given loading value, this way when all the parallel calls are done the loader will be not shown otherwise if the array carries at least one entry then also the loader will be displayed indicating operation in progress.

For this will first create one constant file, to hold these loading values, will create this constant class in separate folder called "constants" and filed named "loader.constant":

export class LoaderConstant {
  static readonly GET_NOTES = 'GET_NOTES';
  static readonly ADD_NOTE = 'ADD_NOTE';
  static readonly DELETE_NOTE = 'DELETE_NOTE';
}

Updating the Loader Service, using the array-based loader variable:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class LoaderService {
  private _loaders: string[] = [];

  get loader() {
    return this._loaders.length > 0;
  }

  show(value: string) {
    this._loaders.push(value);
  }

  hide(value: string) {
    this._loaders = this._loaders.filter((loader) => loader !== value);
  }
}

Here, we have made the "_loaders" variable as array of string. The boolean getter loader will return true if "_loaders" array has value, otherwise false.

Update the Note Service code with below method call changes:

this.loaderService.show(LoaderConstant.GET_NOTES);
this.loaderService.hide(LoaderConstant.GET_NOTES);

this.loaderService.show(LoaderConstant.ADD_NOTE);
this.loaderService.hide(LoaderConstant.ADD_NOTE);

this.loaderService.show(LoaderConstant.DELETE_NOTE);
this.loaderService.hide(LoaderConstant.DELETE_NOTE);

Post changes, the Note Service will look like this:

import { Injectable } from '@angular/core';
import { Note } from '../models/note.model';
import { LoaderConstant } from '../constants/loader.constant';
import { LoaderService } from './loader.service';
import { NoteApiService } from '../http/note-api.service';
import { catchError, finalize, Observable, of, tap } from 'rxjs';

@Injectable({...})
export class NoteService {
  getNotes(): Observable<Note[]> {
    this.loaderService.show(LoaderConstant.GET_NOTES);
    return this.noteApiService.getNotes().pipe(
      tap((notes) => (this.notes = notes)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide(LoaderConstant.GET_NOTES))
    );
  }

  addNote(note: Note): Observable<Note> {
    this.loaderService.show(LoaderConstant.ADD_NOTE);
    return this.noteApiService.addNote(note).pipe(
      tap((newNote) => this.notes.push(newNote)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide(LoaderConstant.ADD_NOTE))
    );
  }

  deleteNoteById(id: string): Observable<void> {
    this.loaderService.show(LoaderConstant.DELETE_NOTE);
    return this.noteApiService.deleteNoteById(id).pipe(
      tap(() => this.notes = this.notes.filter((n) => n.id !== id)),
      catchError(this.handleError),
      finalize(() => this.loaderService.hide(LoaderConstant.DELETE_NOTE))
    );
  }
  
  handleError(error: any) {
    alert(`Error Occurred: ${error.message}`);
    return of(error);
  }
}

Run the app to verify if the changes done so far is working fine or not before moving ahead.

Using Reactive State

It's time to shift toward Reactive Programming, will make our notes array state variable present in the Note Service reactive. Reactive programming using RxJS in Angular provide many benefits to maintain and react on state changes. In the beginning the concept of RxJS seems extremely tough to digest, but over a period of time one understands the need of it.

Currently, the notes array of Note Service is shared and accessed among components, but this state variable can't trigger any event in case if changes occurred, only the value update in the template is respected nothing else. If the notes array becomes reactive state, we can even use the async pipe in the component to auto unsubscribe the subscription (we will see the unsubscribing part in the next section).

1. Create Reactive State

To make the state reactive we need to change its type to Subject or BehaviorSubject (for now consider them similar to Observables, checkout more related to Subject, BehaviorSubject and Observables), we follow the convention and add a dollar ($) symbol at the end of notes variable to understand it's an Observable. Below shows the necessary changes required for making notes of type BehaviorSubject:

import { BehaviorSubject } from 'rxjs';

notes$ = new BehaviorSubject<Note[]>([]);

Here, the notes$ variable is a BehaviorSubject of type Note[] array.

Further, the changes related to notes array will then be reflected as below:

// getNotes
tap((notes) => this.notes$.next(notes))

// addNote
tap((newNote) => this.notes$.next([...this.notes$.value, newNote]))

// deleteNoteById
tap(() => this.notes$.next(this.notes$.value.filter((note) => note.id !== id)))

Here, we are now using the notes$ reactive state for the get, add and delete operations happening in the tap operator. To update the state, we need to call the next method and to get current state value we need to use the value property.

2. Consume Reactive State

Time to reflect the reactive state notes$ in the list and empty components, in order to use the notes$ state in the template directly we need to use the async pipe.

Below shows the template and TypeScript component code for list component:

// update getter for the notes$
get notes$() {
  return this.noteService.notes$;
}

// use async pipe with notes$
<div *ngFor="let note of (notes$ | async)?.reverse()">
    <app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
</div>

Here, we have updated the getter of list component and used async pipe for notes$.

Below shows the template and TypeScript component code for empty component:

// update getter for the notes$
get notes$() {
  return this.noteService.notes$;
}

// use async pipe with notes$
<div *ngIf="(notes$ | async)?.length === 0" class="text-center p-3 text-muted">
  <h1 class="display-4 text-secondary">
    <i class="bi bi-egg me-2"></i>
  </h1>
  No notes found
</div>

Here, we have updated the getter of empty component and used async pipe for notes$.

Always Unsubscribe

Forget your ex but never forget to unsubscribe the subscription in Angular. To unsubscribe we need to call the unsubscribe method on ngOnDestroy lifecycle event of the component. This is hectic task because then you need to create that many subscriptions object, which can be easily produce human errors.

To simplify the unsubscribe activity we can use the takeUntil operator along with a Subject and apply on the Note Service method calls at component and will call the Subject's complete method which completes the observable, and stops the observable to emit further value, thus effectively preventing the memory leak. Let's use the Subject-takeUntil way of unsubscribing the subscriptions in the list and form component.

Below the necessary changes applied to list component:

// 1. import Subject and takeUntil
import { Subject, takeUntil } from 'rxjs';

// 2. create destroy$ Subject
private destroy$ = new Subject<void>();

// 3. complete destroy$ on ngOnDestroy life-cycle event
ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

// 4. add takeUntil to the pipe method of Obsevables
this.noteService.getNotes()
  .pipe(takeUntil(this.destroy$)).subscribe();
this.noteService.deleteNoteById(note.id)
  .pipe(takeUntil(this.destroy$)).subscribe();

Below the necessary changes applied to form component:

// 1. import Subject and takeUntil
import { Subject, takeUntil } from 'rxjs';

// 2. create destroy$ Subject
private destroy$ = new Subject<void>();

// 3. complete destroy$ on ngOnDestroy life-cycle event
ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

// 4. add takeUntil to the pipe method of Obsevables
this.noteService
  .addNote(newNote)
  .pipe(
    takeUntil(this.destroy$),
    finalize(() => (this.content = ''))
  )
  .subscribe();

State Management (Future Scope)

We have seen the default state management way in Angular and the RxJS reactive way as well. Apart from these, there are dedicated libraries for managing state in Angular such as NgRx and Akita. These libraries can be used to efficiently manage the states in Angular. They come with their own ideology and state management approaches, that can be further studied.

The new Angular (v17 onwards) offers a new way of managing and creating such reactive states using Signals, we will touch base regarding the state management in future. For now as a beginner, we can take baby steps to understand state management as it's a very vast topic requires a heavy learning investment and time.

The Notesy App so far is now adapted to reactive way a little bit and the app is also added to the same repository to the folder named "ng-5-rxjs".


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.