26 Jun 2023
4 min

Angular Signals RxJS Interop From a Practical Example

Signals are Angular’s new reactive primitive that will improve the way we develop Angular Apps and the Developer’s Experience. It will also make a big difference in the change detection mechanism. 

In this article, I’ll explain how to create an Angular Typeahead component based on signals. Through this practical example, you will learn:

  • How to convert an RxJS observable to a signal
  • How to convert a signal to an observable

We love visuals, so let’s see what we are about to build.

The code below is the starting point. We will improve it by applying bindings based on signals.

<div class="page-container">
  <mat-form-field class="page-container--form-field">
    <mat-label>Enter User Id (Empty will fetch all)</mat-label>
    <input matInput type="text" [matAutocomplete]="autoComplete" />

    <mat-spinner
      *ngIf="false"
      matSuffix
      class="page-container--spinner"
    ></mat-spinner>

    <mat-autocomplete #autoComplete="matAutocomplete">
      <mat-option> Option 1 </mat-option>
      <mat-option> Option 2 </mat-option>
      <mat-option> Option 3 </mat-option>
    </mat-autocomplete>
  </mat-form-field>
</div>

Convert an RxJS observable to a signal

Let’s start small by getting some data from a service, converting them to a signal, and iterating over the items constructing the autocomplete options.

import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, delay, throwError } from 'rxjs';
import { Post } from './post.type';


@Injectable({
  providedIn: 'root',
})
export class PostsService {
  private http = inject(HttpClient);

  get(userId?: number): Observable<Post[]> {
    if (userId == 100) {
      return throwError(() => new Error('User not found'));
    }
    return this.http
      .get<Post[]>('https://jsonplaceholder.typicode.com/posts', {
        params: {
          ...(userId ? { userId: userId.toString() } : {}),
        },
      })
      .pipe(delay(2000));
  }
}

The get() method: 

  • Accepts a userId argument 
  • Throws an error if the userId is 100 (this is just a manual way to throw an error so that we can see how to apply basic error handling) 
  • Applies an artificial delay to help us display the loading indicator

But as mentioned above, we will see all of this step by step.

In the code below, we are using the toSignal method, which accepts an observable and returns a Signal based on the values produced by the observable. Please note the subscription to that observable is managed automatically for us, and it is cleared up when the injection context is getting destroyed.

@Component({...})
export class PostsComponent {
  private postsService = inject(PostsService);
  posts = toSignal(this.postsService.get());
}

Now let’s iterate over the post items in the HTML template. Note that we have to use the parenthesis to get the values from the Signal. This might sound like a bad approach since we’ve learned that binding to a method is getting called on every change detection cycle. However, this is not the case with signals. 

<mat-autocomplete #autoComplete="matAutocomplete">
  <mat-option *ngFor="let post of posts()" [value]="post.title">
    {{ post.title }}
  </mat-option>
</mat-autocomplete>

So far, we managed to complete the first step by only using the toSignal method. Now let’s try getting the user input and sending it to the service.

Convert a signal to an observable

The binding to the input field will be based on a signal as well. We will name that unserID with undefined being the initial value.

export class PostsComponent {
  private postsService = inject(PostsService);
  userId = signal<number | undefined>(undefined);
  posts = toSignal(this.postsService.get());
}

We need to apply a two-way binding, but since we cannot apply a banana in a box yet, we will separate the property binding using the event binding. (Note, at the time of writing this article, the Angular version is 16.0.4)

<input
  [ngModel]="userId()"
  (ngModelChange)="userId.set($event)"
  matInput
  type="text"
  [matAutocomplete]="autoComplete"
/>

To send an HTTP call whenever the userId value changes, we can use a combination of the effect method and a BehaviorSubject, as seen below. 

import { effect, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';

@Component({...})
export class PostsComponent {
  private postsService = inject(PostsService);
  userId = signal<number | undefined>(undefined);
  userId$ = new BehaviorSubject<number | undefined>(undefined);

  constructor() {
    this.userId$
      .pipe(
        // Do something here,
        takeUntilDestroyed()
      )
      .subscribe();

    effect(() => {
      this.userId$.next(this.userId());
    });
  }
}

While this approach works fine, it’s a bit verbose. It also requires the developer to subscribe/unsubscribe.

However, this seems to be a pattern we can use whenever we are combining a Signal along with some RxJS operators. That’s why the Angular Development Team created the toObservable method, which converts a signal to an observable and automatically handles the subscription/unsubscription. With this method, the code will become less verbose.

export class PostsComponent {
  private postsService = inject(PostsService);
  userId = signal<number | undefined>(undefined);
  private posts$ = toObservable(this.userId).pipe(
    switchMap((userId) => this.postsService.get(userId))
  );
  posts = toSignal(this.posts$);
}

In the code above, we have the posts$ class field, which has a very short lifecycle since this is getting converted to a signal using the toSignal method. Let’s improve that a bit and also use a debounceTime operator.

export class PostsComponent {
  private postsService = inject(PostsService);
  userId = signal<number | undefined>(undefined);
  posts = toSignal(
    toObservable(this.userId).pipe(
      debounceTime(500),
      switchMap((userId) => this.postsService.get(userId))
    )
  );
}

Since we have converted the signal to an observable, we can apply RxJS operators to handle the loading state along with the error handling. We are handling the loading state with the isLoading signal<boolean>.

export class PostsComponent {
  private postsService = inject(PostsService);
  isLoading = signal<boolean>(false);
  userId = signal<number | undefined>(undefined);
  posts = toSignal(
    toObservable(this.userId).pipe(
      debounceTime(500),
      tap(() => this.isLoading.set(true)),
      switchMap((userId) =>
        this.postsService.get(userId).pipe(catchError(() => of([])))
      ),
      tap(() => this.isLoading.set(false))
    )
  );
}

And that’s it! . 

I’ll finish this article by sharing some suggestions:

  • Try not to use the async pipe on the HTML template, as this will increase the change detection cycles
  • Try converting your component’s state to a signal
  • Don’t be afraid to use RxJS operators
  • Try using the effect on side effects that require logging or DOM manipulation.

You can find also a video that describes this example on my YouTube channel: Learn Angular Signals RxJS Interop From a Practical Example

Thanks for reading my article. 

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.