admin管理员组

文章数量:1433485

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?
Share Improve this question edited Apr 7 at 9:20 jonrsharpe 122k30 gold badges268 silver badges476 bronze badges asked Nov 18, 2024 at 16:50 Mo3biusMo3bius 6621 gold badge8 silver badges19 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

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.

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

本文标签: