Observables to signals using toSignal in Angular

Observables to signals using toSignal in Angular

Reactivity in Angular has significantly evolved, especially with the introduction of Signals. In this article, we will discuss how to work with Angular, Reactivity, Signals, and RxJS. You will learn how to transform observables into Signals using the toSignal function, making it easier to transition and reuse existing code in your Angular applications.

ToSignal: The Transition Function

This function allows converting RxJS observables into Signals. This facilitates the reuse of existing code while embracing this new model of reactivity. toSignal is especially useful for maintaining business logic in RxJS while using Signals for rendering and other aspects of the UI.

Basic Implementation of ToSignal

Creating an Observable

Let’s look at a simple example where we create a basic observable in Angular:

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <h2>state$: {{ state$ | async }}</h2>
    <input type="text" #input>
    <button (click)="change(input.value)">Change state</button>
  `,
})
export class AppComponent {
  state$ = new Subject<string>();

  change(newValue: string) {
    this.state$.next(newValue);
  }
}

Transforming the Observable into a Signal

Now, let’s transform this observable into a Signal using the toSignal function:

import { Component } from '@angular/core';
import { Subject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h2>state$: {{ state() }}</h2>
    <input type="text" #input>
    <button (click)="change(input.value)">Change state</button>
  `,
})
export class AppComponent {
  state$ = new Subject<string>();
  state = toSignal(this.state$);

  change(newValue: string) {
    this.state$.next(newValue);
  }
}

Using toSignal, we have transformed the state$ observable into a Signal state and can avoid using the AsyncPipe in the template.

Options for the ToSignal Function

Initial Value

This option is used when working with observables that do not have an initial value. It allows setting an initial value so that the Signal always has a value to render. This prevents the Signal from being undefined until the observable emits a value.

Check out the following example:

import { computed } from '@angular/core';
import { Subject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

const state$ = new Subject<string>();
const state = toSignal(state$);
const doubleState = computed(() => state().repeat(2));

state(); // undefined

In this case, state is undefined until state$ emits a value and if we try to duplicate the value of state using a computed, we will get an alert from TypeScript notifying that state is undefined.

mardown

To avoid this, we can use the initialValue option:

import { Subject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { computed } from '@angular/core';

const state$ = new Subject<string>();
const state = toSignal(state$, { initialValue: 'value from signal' });
const doubleState = computed(() => state().repeat(2));

state(); // value from signal
doubleState(); // value from signalvalue from signal

Require Sync

This option synchronizes the Signal with the initial value of the observable if it has an initial value. Very useful when we need the Signal to have the same value as the observable from the beginning.

import { BehaviorSubject } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

const state$ = new BehaviorSubject('value from observable');
const state = toSignal(state$, { requireSync: true });

state(); // 'value from observable'

Reject Errors

In case the observable generates an error, this option allows the Signal to keep its last valid value. Imagine we have an observable that emits values every second and, after 5 seconds, throws an error. If we do not use rejectErrors, the Signal will stop and will not emit more values. However, if we use rejectErrors, the Signal will keep the last valid value.

import { interval, tap } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

const interval$ = interval(1000)
.pipe(
  tap((value) => {
    if (value === 5) {
      throw new Error('Something went wrong');
    }
    return value;
  })
);

const intervalValue = toSignal(interval$, { initialValue: 0, rejectErrors: true });

intervalValue(); // 5

Real Example: Integration with HTTP Client

Now, let’s look at a more realistic example of how to transform an observable from an HTTP service into a Signal.

HTTP Service

Suppose we have a service that obtains data from an API with the method getLocations which would be a RxJS observable without an initial value, meaning the value will transmit when the HTTP request completes.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

export interface Location {
  id: number;
  name: string;
  description: string;
  latitude: number;
  longitude: number;
}

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

  getLocations(){
    const path = `https://api.nicobytes.store/api/v1/locations`;
    return this.http.get<Location[]>(path);
  }
}

Now let’s transform the service’s observable into a Signal so that the component can render the API data.

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { DataService } from './data.service';

@Component({
  selector: 'app-data',
  standalone: true,
  template: `
    @for(item of locations(); track item.id){
      <li>{{ item.name }}</li>
    }
  `,
})
export class DataComponent {
  dataService = inject(DataService);

  locations$ = this.dataService.getLocations();
  locations = toSignal(this.locations$, {
    initialValue: []
  });
}

Conclusion

The toSignal function facilitates the adoption and utilization of Angular’s new reactivity model without having to discard the robust logic built with RxJS. The key is to identify the appropriate use cases and employ options like initialValue, requireSync, and rejectErrors according to the project’s needs.

Implementing Signals can transform the way your Angular applications handle reactivity, providing more granular performance and simpler state management for the UI.