Managing State Using RxJS Subjects in Angular Applications

RxJS has been around for a while now and it is still the most popular library for reactive programming.RxJS is a library that was created with the intent of simplifying asynchronous programming by using observable sequences and operators.

RxJS provides a single source of truth for data and events. This makes it easy to manage data and state in the application. RxJS also provides performance benefits as it can be used to make an application more efficient by handling some of the heavy liftings that would otherwise require the use of expensive DOM operations or server requests.

By the end of this post, you will be able to understand the following things

  • How to build Angular apps using Observable Data Services?
  • How to use Observable in the angular component?
  • What is the difference between Subject and BhaviorSubject

What is state management?

The data on the interface is known as the state; it exists in memory and must be synced to the database. State management is concerned with how that data is synced, shared, and updated.

Cookies, Local or Session Storage, Query Strings, and hidden fields are all methods for managing state in front-end applications. These techniques are extremely difficult to maintain. Using third-party libraries like ‘NGRX’ or ‘Observable’ as data services is a better way to maintain the state in Angular.

In this post, I’ll show you how to build a state service with RxJs. To keep the demo simple, I’m making a simple todo application where users can add, update, and delete tasks.

The architecture of the application is shown in the following image. Here I am creating a todo service that has two BehaviorSubject one for the list of todo and another for the count of todo.

Why BehaviorSubject?

BehaviorSubject is a type of subject, and a subject is a subset of an observable, so you can subscribe to messages just like any other observable. The following are BehaviorSubject's distinguishing characteristics: Source

  • It requires an initial value because it must always return a value on subscription even if no next method is invoked.
  • It returns the subject’s most recent value upon subscription. A regular observable only fires when it receives a next command.
  • The getValue() method can be used to retrieve the last value of the subject in non-observable code at any time.

I assume that you are familiar with angular CIL and how to create and run the angular applications

For this demo I am using stackbliz cloud IDE. First, create a new application in stackbliz

  • Add a todo.service and add the following
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import Todo, * as todos from './todo';

@Injectable({
  providedIn: 'root',
})
export class TodoService {
  todos: Todo[] = [
    { id: 1, title: 'Todo 1', completed: false },
    { id: 2, title: 'Todo 2', completed: false },
    { id: 3, title: 'Todo 3', completed: false },
  ];

  private readonly todosSubject = new BehaviorSubject<Todo[]>(this.todos);
  private readonly todosCountSubject = new BehaviorSubject<number>(0);

  public todosCount$ = this.todosCountSubject.asObservable();
  public todos$ = this.todosSubject.asObservable();

  constructor() {
    this.todosCountSubject.next(this.todos.length);
  }

  addTodo(todo: Todo) {
    this.todos.push(todo);
    this.todosSubject.next(this.todos);
    this.todosCountSubject.next(this.todos.length);
  }

  toggleTodo(id: number) {
    const todo = this.todos.find((todo) => todo.id === id);
    todo!.completed = !todo!.completed;
    this.todosSubject.next(this.todos);
  }
  //deleteTodo
  deleteTodo(id: number) {
    const todo = this.todos.find((todo) => todo.id === id);
    this.todos.splice(this.todos.indexOf(todo!), 1);
    this.todosSubject.next(this.todos);
    this.todosCountSubject.next(this.todos.length);
  }

  updateTodo(todo: Todo) {
    const index = this.todos.findIndex((t) => t.id === todo.id);
    this.todos[index] = todo;
    this.todosSubject.next(this.todos);
  }
}

The above code is the self-explanatory only thing that I want to highlight is the below code.

 private readonly todosSubject = new BehaviorSubject<Todo[]>(this.todos);
  private readonly todosCountSubject = new BehaviorSubject<number>(0);

I declared two private read-only properties of type BehaviorSubject because we don’t want clients to see them. Then I added two public properties, which are shown below. The client will make use of these public properties to get the data.

  public todosCount$ = this.todosCountSubject.asObservable();
  public todos$ = this.todosSubject.asObservable();

It is now time to expose some public properties/methods for clients, such as addTodo. Here, I’m adding a new todo and then notifying all subscribers about the new data.

The subject next method is used to send messages to an observable, which are then distributed to all angular components that are subscribers (also known as observers) to that observable.

  addTodo(todo: Todo) {
    this.todos.push(todo);
    this.todosSubject.next(this.todos);
    this.todosCountSubject.next(this.todos.length);
  }

Similarly way I have added other methods

Our todo state service is now available for use. To use this service, first include it in the main module and then inject it into the component constructor

todo-list.component.ts

import { TodoService } from './../todo.service';
import { Component, OnInit } from '@angular/core';
import Todo from '../todo';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.scss'],
})
export class TodoListComponent implements OnInit {
  todos: Todo[] = [];
  todo: Todo = { id: 0, title: '', completed: false };
  constructor(public todoSvc: TodoService) {}

  ngOnInit(): void {
    this.todoSvc.todos$.subscribe((todos) => {
      this.todos = todos;
    });
  }

  //addTodo
  addTodo() {
    const todo = {
      id: Math.floor(Math.random() * 1000),
      title: this.todo.title,
      completed: false,
    };
    this.todoSvc.addTodo(todo);
  }
  //toggleCompleted
  toggleCompleted(todo: Todo) {
    todo.completed = !todo.completed;
    this.todoSvc.updateTodo(todo);
  }
  //remove
  remove(todo: Todo) {
    this.todoSvc.deleteTodo(todo.id);
  }
}

Below is the markup for adding and deleting todo. The below code is self-explanatory and easy to understand.

todo-list.component.html`

<!-- add todo form -->
<form (ngSubmit)="addTodo()" #todoForm="ngForm">
  <input
    type="text"
    name="title"
    [(ngModel)]="todo.title"
    #title="ngModel"
    required
  />
  <button type="submit" [disabled]="!todoForm.form.valid">Add Todo</button>
</form>
<!-- display count of todos-->
<p>Number of todos: {{ todoSvc.todosCount$ | async }}</p>

<!-- display todos -->
<ul>
  <li *ngFor="let todo of todos">
    <input
      type="checkbox"
      [checked]="todo.completed"
      (change)="toggleCompleted(todo)"
    />
    <span [ngClass]="{ 'todo-completed': todo.completed }">{{
      todo.title
    }}</span>
    <button (click)="remove(todo)">x</button>
  </li>
</ul>

I only want to talk about the following code. As you are aware, we have exposed two public observables from the service. one for the todo list and another for the todo count In this case, I’m directly subscribing to the todosCount$ Observable in the html. Another option is to declare the property in the component and subscribe to the todosCount$ Observable to update the property. The benefit of following is that angular automatically subscribes and unsubscribes the observable.

<!-- display count of todos-->
<p>Number of todos: {{ todoSvc.todosCount$ | async }}</p>

todo-list.component.scss

.todo-completed{
text-decoration: line-through;
}

todo.ts

export default interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

source code

Please do not post any spam link in the comment box😊

إرسال تعليق (0)
أحدث أقدم