I have an Angular app that makes multiple HTTP API calls. I combine the results of these APIs into a single stream of data, which is then passed as inputs to child components. In the template, I use the async
pipe to subscribe to the stream and only display the child components when the data is available.
Here’s the code I’m working with:
ngOnInit(): void {
this.getData();
}
getData() {
const apiDataOne$ = this.http.get<One[]>(URLOne);
const apiDataTwo$ = this.http.get<Two[]>(URLTwo);
this.dataStream$ = forkJoin([apiDataOne$, apiDataTwo$]).pipe(
map(([result1, result2]) => ({ result1, result2 }))
);
}
updateData() {
this.getData();
}
@if(dataStream$ | async as dataStream){
<child-component [input1]="dataStream.result1" [input2]="dataStream.result2">
</child-component>
}
I want to trigger the getData()
method dynamically, such as when a user clicks a button. However, when I call getData()
again, it resets the dataStream$
observable. This causes the async pipe to emit null
or undefined
for a short time, which results in the child component being destroyed and recreated.
I want to prevent the child component from being destroyed and recreated, even when the data is re-fetched.
We have to make sure the root observable does not complete or get reset. We can use a BehaviorSubject
to be used to trigger reloads when the next
method is called. The other thing about BehaviorSubject
is that it triggers the stream during initial subscription.
@Component({
selector: 'app-root',
standalone: true,
imports: [Child, CommonModule],
template: `
@if(dataStream$ | async; as dataStream){
<app-child [input1]="dataStream.result1" [input2]="dataStream.result2">
</app-child>
<button (click)="updateData()"> update</button>
}
`,
})
export class App {
private loadTrigger: BehaviorSubject<string> = new BehaviorSubject<string>('');
dataStream$: Observable<ApiResonses> = this.loadTrigger.pipe(
switchMap(() =>
forkJoin<{
result1: Observable<Array<any>>;
result2: Observable<Array<any>>;
}>({
result1: of([{ test: Math.random() }]),
result2: of([{ test: Math.random() }]),
})
)
);
ngOnInit(): void {}
updateData() {
this.loadTrigger.next(''); // <- triggers refresh
}
}
So the button click calls the updateData
method which calls the BehaviorSubject
's next
method, which triggers the other API to reevaluate. To switch from the BehaviorSubject
observable to the forkJoin
we can use switchMap
.
The fork join has a convenient object syntax as shown in the example, so there is no need for the map
operation.
import { Component, input } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { BehaviorSubject, forkJoin, of, switchMap, Observable } from 'rxjs';
import { CommonModule, JsonPipe } from '@angular/common';
export interface ApiResonses {
result1: any[];
result2: any[];
}
@Component({
selector: 'app-child',
standalone: true,
imports: [JsonPipe],
template: `
{{input1() | json}}
<br/>
<br/>
{{input2() | json}}
`,
})
export class Child {
input1: any = input();
input2: any = input();
ngOnDestroy() {
alert('destroyed');
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [Child, CommonModule],
template: `
@if(dataStream$ | async; as dataStream){
<app-child [input1]="dataStream.result1" [input2]="dataStream.result2">
</app-child>
<button (click)="updateData()"> update</button>
}
`,
})
export class App {
private loadTrigger: BehaviorSubject<string> = new BehaviorSubject<string>(
''
);
dataStream$: Observable<ApiResonses> = this.loadTrigger.pipe(
switchMap(() =>
forkJoin<{
result1: Observable<Array<any>>;
result2: Observable<Array<any>>;
}>({
result1: of([{ test: Math.random() }]),
result2: of([{ test: Math.random() }]),
})
)
);
ngOnInit(): void {}
updateData() {
this.loadTrigger.next(''); // <- triggers refresh
}
}
bootstrapApplication(App);