Observable emits synchronous value in the toSignal function with the requireSync option

Connie Leung - Sep 30 - - Dev Community

The return type of the toSignal function is Signal<T | undefined>. Observable is lazy and emits the first value when an event occurs. Therefore, the signal is undefined until the Observable emits the first value. If the toSignal function wants the Observable to emit synchronously, like BehaviorSubject or startWith, it can provide the requireSync: true option to the second argument.

In this blog post, I will display two use cases of the requireSync option.

Use cases

  • the HttpClient queries a person by id, and the startWith operator provides an initial value.
  • An Angular component has buttons that update a BehaviorSubject's value when clicked.

Emit an initial value with RxJS startWith

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}
Enter fullscreen mode Exit fullscreen mode
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { catchError, Observable, of, startWith } from "rxjs";
import { Person } from "./person.type";

const URL = 'https://swapi.dev/api/people';

const DEFAULT: Person = {
 name: '',
 height: '',
 mass: '',
 hair_color: '',
 skin_color: '',
 eye_color:  '',
 gender: '',
 films: [],
};

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

 getData(id: number): Observable<Person> {
   return this.http.get<Person>(`${URL}/${id}`).pipe(
     startWith(DEFAULT)
     catchError((err) => {
       console.error(err);
       return of(DEFAULT);
     }));
 }
}
Enter fullscreen mode Exit fullscreen mode

Create a StarWarService with a getData method to call the StarWar API to retrieve a person. The HttpClient emits the result to the startWith operator that returns an initial value. Therefore, the return type of the method is Observable<Person>.

Pass requireSync option to toSignal

import { ChangeDetectionStrategy, Component, inject, Injector, input, OnChanges, Signal } from '@angular/core';
import { StarWarService } from './star-war.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { StarWarPersonComponent } from './star-war-person.component';
import { Person } from './person.type';

@Component({
 selector: 'app-star-war',
 standalone: true,
 imports: [NgStyle, StarWarPersonComponent],
 template: `
   <h3>Star War Jedi vs Sith</h3>
    <app-star-war-person [person]="light()" />
    <app-star-war-person [person]="evil()" />
   </div>
 `,
})
export class StarWarComponent implements OnChanges {
 // required signal input
 jedi = input.required<number>();

 // required signal input
 sith = input.required<number>();

 starWarService = inject(StarWarService);
 injector = inject(Injector);
 light!: Signal<Person>;
 evil!: Signal<Person>;

 ngOnChanges(): void {}
}
Enter fullscreen mode Exit fullscreen mode

In the StarWarComponent component, I inject the StarWarService and the component's injector. Moreover, I declare light and evil Signals to store the results returned from the toSignal function. Observe that the Signals drop the undefined in the type.

interface ToSignalOptions<T> {
 initialValue?: unknown;
 requireSync?: boolean;
 injector?: Injector;
 manualCleanup?: boolean;
 rejectErrors?: boolean;
 equal?: ValueEqualityFn<T>;
}
Enter fullscreen mode Exit fullscreen mode

The ToSignalOptions option has a requireSync property, which I use to ensure the Observables emit values when subscribed.

export class StarWarComponent implements OnChanges {
  same as before 


 ngOnChanges(): void {
   this.light = toSignal(this.starWarService.getData(this.jedi()), {
     injector: this.injector,
     requireSync: true,
   });

   this.evil = toSignal(this.starWarService.getData(this.sith()), {
     injector: this.injector,
     requireSync: true
   });
 }
}
Enter fullscreen mode Exit fullscreen mode

In the ngOnChanges method, I call the service to obtain the Observables, and use the toSignal function to create the signals. The second argument is an option with the component's injector and requireSync.

<app-star-war-person [person]="light()" kind="Jedi Fighter" />
<app-star-war-person [person]="evil()" kind="Sith Lord" />
Enter fullscreen mode Exit fullscreen mode

Next, I pass the light and evil signals to the StarWarPersonComponent component to display the details of a Jedi fighter and a Sith lord.

Use BehaviorSubject in toSignal

import { Route } from '@angular/router';

export const routes: Route[] = [
 {
   path: 'requireSync-example',
   loadComponent: () => import('./require-sync/example.component'),
   data: {
     btnValues: [-5, -3, 1, 2, 4]
   }
 },
];
Enter fullscreen mode Exit fullscreen mode

In the routes array, the route data of requireSync-example path is an array of numbers.

export const appConfig = {
 providers: [
   provideRouter(routes, withComponentInputBinding()),
 ]
}
Enter fullscreen mode Exit fullscreen mode

In the appConfig, the withComponentInputBinding feature of the provideRouter function binds the route data to the required signal input of the ExampleComponent component.

@Component({
 selector: 'app-requireSync-example',
 standalone: true,
 template: `
   <div>
     @for (v of btnValues(); track v) {
       <button (click)="update(v)">{{ v }}</button>
     }
   </div>
   <div>
     <p>total: {{ total() }}</p>
     <p>source: {{ source.getValue() }}</p>
     <p>sum: {{ sum() }}</p>
   </div>
   <button (click)="changeArray()">Update the BehaviorSubject</button>
 `,
})
export default class ExampleComponent {
 btnValues = input.required<number[]>();
 something = new BehaviorSubject(0);
 total = toSignal(this.something, { requireSync: true });

 source = new BehaviorSubject([1,2,3,4,5]);
 sum = toSignal(
   this.source.pipe(map((values) => values.reduce((acc, v) => acc + v, 0))), { requireSync: true });

 update(v: number) {
   this.something.next(this.something.getValue() + v);
 }

 changeArray() {
   const values = this.source.getValue().length <= 5 ? [11,12,13,14,15,16,17,18] : [1,2,3,4,5];
   this.source.next(values);
 }
}
Enter fullscreen mode Exit fullscreen mode

Something is a BehaviorSubject with an initial value of 0, and the toSignal function creates a signal from it. The requireSync option is possible because the BehaviorSubject can emit a value immediately when it is subscribed. When clicked, the buttons call the update method to update the BehaviorSubject. The HTML template displays the total signal when it receives a new value.

Source is another BehaviorSubject that stores an array of numbers. Then, the BehaviorSubject emits to the map operator to calculate the sum. The toSignal function and requireSync: true assert the stream to emit the sum when subscribed. The button click executes the changeArray method to alternate the array of source. Since the sum signal consumes the stream, the template renders the new value of source and sum.

Conclusions:

  • requireSync asserts the Observable emits a value immediately when subscribed.
  • We can pass the requireSync option to the toSignal function when the Observable is BehaviorSubject or consists of RxJS operators that produce values such as startWith or of.
  • If toSignal has requireSync: true but the Observable does not emit a value immediately, an error is thrown.

References:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .