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:
Component Code:
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();
}
HTML Template:
@if(dataStream$ | async as dataStream){
<child-component [input1]="dataStream.result1" [input2]="dataStream.result2">
</child-component>
}
Issue:
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.
Questions:
- How can I prevent the child component from being destroyed and recreated when the data stream is updated?
- Is there a way to update the observable without causing the child components to be destroyed?
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:
Component Code:
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();
}
HTML Template:
@if(dataStream$ | async as dataStream){
<child-component [input1]="dataStream.result1" [input2]="dataStream.result2">
</child-component>
}
Issue:
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.
Questions:
- How can I prevent the child component from being destroyed and recreated when the data stream is updated?
- Is there a way to update the observable without causing the child components to be destroyed?
2 Answers
Reset to default 2We 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.
Full Code:
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);
Stackblitz Demo
Whether you are using the new @if
or ngIf
directives, I believe whatever content is within, they are destroy since the condition is re-evaluated and by definition, they are structural directives by adding or removing content in the DOM.
I am not sure exactly what is the requirement but you could solve it in a simple manner handling a css
rule, with this, the child component doesn't get destroy each time getData
is called (again, I am not sure if this approach is acceptable for you):
parent component
@Component({...})
export Class ParentComponent implements OnInit {
protected loading = signal<boolean>(false);
protected dataStream$!: Observable<{ result1: number; result2: number }>;
ngOnInit(): void {
this.getData();
}
getData() {
this.loading.set(true);
const apiDataOne$ = of(777).pipe(delay(500));
const apiDataTwo$ = of(888).pipe(delay(100));
this.dataStream$ = forkJoin([apiDataOne$, apiDataTwo$]).pipe(
map(([result1, result2]) => ({ result1, result2 })),
tap(() => this.loading.set(false))
);
}
}
parent HTML component
<h1>Parent Component</h1>
@let input1 = (dataStream$ | async)?.result1;
@let input2 = (dataStream$ | async)?.result2;
<child
[ngStyle]="{ 'visibility': loading() ? 'hidden' : 'unset' }"
[input1]="input1"
[input2]="input2" />
<button type="button" (click)="getData()">Fetch again!</button>
child component
@Component({
selector: 'child',
standalone: true,
template: `
<p>{{ input1() }}</p>
<p>{{ input2() }}</p>
`,
})
export class Child implements OnDestroy {
input1 = input<number>();
input2 = input<number>();
ngOnDestroy() {
console.log('destroyed...') // it does not get triggered on getData()
}
}
demo
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745605845a4635667.html
评论列表(0条)