How to detect changes observable in child-component in Angular 14

113 Views Asked by At

In the parent component, I've defined an Observable variable named results$, and in the view, it is utilized as follows:

controller:

this.result$ = this.firstObservable$.pipe(
      combineLatestWith(
        this.secondObservable$.pipe(filter((task) => !!task.key)),
      ),
      switchMap(([first, second]: [type1, type2]) => {

        // some logic here happend

      map((businessFunctionResult: businessFunctionType[]) => ({
        data: businessFunctionResult,
        isLoading: false,
      })),
      catchError((error: unknown) => {
        const err = error as HttpErrorResponse;
        return of({ error: err, isLoading: false });
      })
     
    );

view:

<div *ngIf="result$ | async as result">
  <container *ngIf="!result.isLoading && result.data">
    <child-component1 [data]="result.data"></child-component1>
    <child-component2 [data]="result.data"></child-component2>
    ...
  </container>
</div>

In the child-component, an Input is present, and each child component implements the OnInit lifecycle hook. However, when the value of results changes or updates, the child components do not reflect these changes.

export class ChildComponent1
  implements OnInit
{
  
  @Input() data!: any[];
ngOnInit(): void {
    console.log(this.data);
}
}

An alternative solution involves replacing ngIf with ngFor in the code. By adopting this approach, the child components are successfully updated. While this solution may seem less straightforward, but its not such a clear solution because the component will be initialized every time.

<div *ngFor="let result of [result$ | async]">
  <container *ngIf="!result.isLoading && result.data">
    <child-component1 [data]="result.data"></child-component1>
    <child-component2 [data]="result.data"></child-component2>
  </container>
</div>

I know I can use ngOnChanges in child components, but for some issues (refactoring, etc.) I don't want to do it.

I want to update the child components every time result$ has a new value.

stackblitz

2

There are 2 best solutions below

0
Daniel Vágner On

What about to work with behaviroSubject instead of input ?

Like I would create service with behaviorSubject.

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable()
export class DataService {

  private messageSource = new BehaviorSubject('default message');
  currentMessage = this.messageSource.asObservable();

  constructor() { }

  changeMessage(message: string) {
    this.messageSource.next(message)
  }

}

And in parent component you could set new value or even in child. It works both way.

export class ParentComponent implements OnInit, OnDestroy {

  message:string;
  subscription: Subscription;

  constructor(private data: DataService) { }

  ngOnInit() {
    this.subscription = this.data.currentMessage.subscribe(message => this.message = message)
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

And listen in one of that component.

export class ChildOrSiblingComponent implements OnInit, OnDestroy {

  message:string;
  subscription: Subscription;

  constructor(private data: DataService) { }

  ngOnInit() {
    this.subscription = this.data.currentMessage.subscribe(message => this.message = message)
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  newMessage() {
    this.data.changeMessage("Hello from Sibling")
  }

Personally, I would probably not use BehaviorSubject for parent-child or child-parent communication.

Rather, I would use it in two components that don't share anything and need to pass data between them. But in your case, it seems like a good solution.

0
BizzyBob On

If you don't want to use ngChanges, you could make your @Input() a setter and do your logic there:

export class ChildComponent {
  public myData: string;

  @Input() set data(value: string) {
    console.log('@Input()', value);
    this.myData = value;
  }
}

Here the setter logic will be executed whenever the input value changes.

StackBlitz