Observable emits synchronous value in the toSignal function with the requireSync option
Connie Leung
Posted on September 30, 2024
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[];
}
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);
}));
}
}
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 {}
}
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>;
}
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
});
}
}
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" />
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]
}
},
];
In the routes
array, the route data of requireSync-example
path is an array of numbers.
export const appConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
]
}
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);
}
}
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:
- toSignal requireSync option: https://angular.dev/guide/signals/rxjs-interop#the-requiresync-option
- ToSignalOptions: https://angular.dev/api/core/rxjs-interop/ToSignalOptions#
- Stackblitz Demo:
Posted on September 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 30, 2024