Angular Renaissance: Part 5 - Reactivity

Oct 11, 2025 | 7 min read | 17 Views

Let us give reactivity touch to our Notesy App which we have created in our Angular renaissance journey so far. In this article, we will explore some of the capabilities of reactive programming using RxJS/Signals for states and will understand some idealism to follow while dealing with async/state related operations.

Our Notesy App is working like a charm, but it has some areas of improvement from reactive programming POV alongside renaissance of Angular. We will transform the Notesy app into a Reactive app by exploring and using concepts related to RxJS (operators, async operations) and Signals (reactive states). We will also follow some best practices to deal with async operations and reactive states. 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 Renaissance: 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
  • 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 { inject, 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 {
  http = inject(HttpClient);
  apiUrl = environment.apiUrl + 'notes';

  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, inject } from '@angular/core';
import { Note } from '../models/note.model';
import { NoteApiService } from '../http/note-api.service';

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

  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, inject } 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 {
  noteApiService = inject(NoteApiService);
  loaderService = inject(LoaderService);
  notes: Note[] = [];

  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.noteService.notes = [...this.noteService.notes, 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(
	takeUntilDestroyed(this.destroyRef),
	finalize(() => (this.content = ''))
      )
      .subscribe();
  }
}

Here, we are using the RxJS finialize operator to rest the content value to empty.

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.noteService.notes = [...this.noteService.notes, 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.

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 and Signals in Angular provide many benefits to maintain and react on state changes. In the beginning the concepts seems extremely tough to digest, but over a period of time one understands the need of it. RxJS used for async operations or streams which provides number of operators, whereas we use Signals to maintain simple state values.

The Angular Signals is most exciting feature, which helps to write reactive states in a convenient way. The Signal concept is a big concept for which a dedicated article is worth to consider, will explore in future.

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. We will see bunch of RxJS operators to handle the async response obtained from the HttpClient along with maintaining the reactive state with notes Signal. 

1. Create Reactive State

To make the state reactive we need to change its type to Signal with generic type as notes of array. Below shows the necessary changes required for making notes of type Signal:

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

notes = signal<Note[]>([]);

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

// getNotes
tap((notes) => this.notes.set(notes))

// addNote
tap((newNote) => this.notes.set([...this.notes(), newNote]))

// deleteNoteById
tap(() => this.notes.set(this.notes().filter((n) => n.id !== id)))

Here, we are now using the notes signal for the get, add and delete operations happening in the tap operator. To update the state, we need to call the set method and to get current state value we need to use the variable with parathesis as method - notes().

We to do the same for loader state as well, in the loader.service.ts file update the boolean variable to Signal of type boolean:

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

private _loader = signal<boolean>(false);

Use this Signal with show/hide methods:

show() { this._loader.set(true); }
hide() { this._loader.set(false); }

2. Consume Reactive State

Time to reflect the reactive state notes Signal in the list component:

@for (note of notes() | reverse; track note.id) {
<app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
} @empty {
<app-empty></app-empty>
}

Here, we are using the notes() as method to obtain the state values reactively.

Use the loader Signal in the loader.component.html as - loader():

@if (loader()) {
<h1 class="loader">
  <i class="bi bi-snow2 text-success"></i>
</h1>
}

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 takeUntilDestroyed operator along with a DestroyRef and apply on the Note Service method calls at component, thus effectively preventing the memory leak. Let's use the DestroyRef -takeUntilDestroyed way of unsubscribing the subscriptions in the list and form component.

Below the necessary changes applied to list component:

// 1. import DestroyRef and takeUntilDestroyed
import { DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// 2. inject DestroyRef
private readonly destroyRef = inject(DestroyRef);

// 3. add takeUntilDestroyed to the pipe method of Obsevables
this.noteService.getNotes().pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
this.noteService.deleteNoteById(note.id).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();

Below the necessary changes applied to form component:

// 1. import DestroyRef and takeUntilDestroyed
import { DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// 2. inject DestroyRef
private readonly destroyRef = inject(DestroyRef);

// 3. add takeUntilDestroyed to the pipe method of Obsevables
this.noteService
  .addNote(newNote)
  .pipe(
    takeUntilDestroyed(this.destroyRef),
    finalize(() => (this.content = ''))
  )
  .subscribe();

State Management (Future Scope)

We have seen the default state management way in Angular and the RxJS/Signals reactive way as well. Apart from these, there are dedicated libraries for managing state in Angular such as NgRx, NgxsAkita, etc. 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-reactive".


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