2
votes

I'm trying to pass async data (an observable) from a parent to child component, but it seems the changes are not being updated on the child component and I'm not sure why.

I have a (simplified) service that is using BehaviourSubject:

@Injectable()
export class MyService {

  private _items: BehaviorSubject<any[]> = new BehaviorSubject([]);
  public items: Observable<any[]> = this._items.asObservable();

  list() {
    this.http.get('...').subscribe(items => {
      this._items.next(items);
    })
  }
}

(The list() function is being called regularly from elsewhere in my app to update the data.)

I then have a parent component:

@Component({
  selector: 'parent',
  templateUrl: '<child [items]="myService.items | async"></child>',
})
export class Parent {

  constructor(public myService: MyService) {
  }
}

and a child:

@Component({
  selector: 'child',
  templateUrl: '<ul><li *ngFor="item of _items">{{item.name}}</li></ul>',
})
export class Parent implements OnChanges {

  @Input() items;
  _items = [];

  constructor() {
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log('changes', changes);
    this._items = changes.items;
  }
}

My understanding is that using the async automatically subscribes/unsubscribes to my Observable and passes any new data down to my child component. When new data comes in, my child component should detect those changes in ngOnChanges and print them out.

  1. My console output is never firing, indicating that no changes are being detected in the child. I've manually subscribed to the Observable in my parent component to verify that new data is coming in so I'm not sure where I'm going wrong here.
  2. Is this the correct was to deal with asynchronous data at the component level?
2
why are you not using SubjectChellappan வ
It seems that this is a bug specific to my setup, as I've been able to create an example of how it should be working stackblitz.com/edit/… (and there's no issue with it)Timmy O'Mahony
So the core of this issue seems to be unrelated to the code above (which, while it can be improved with some of the suggestions below, should be functional). In my app I had a Google map event that was firing the list() function from my service and this seems to be the real underlying problem. I would imagine it's to do with the function being called outside of the Angular run loop (or something similar)Timmy O'Mahony
@TimmyO'Mahony Related link is brokenrmcsharry

2 Answers

5
votes

I could propose you a bit of change in your code :

Parent :

@Component({
  selector: 'app-parent',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: '<app-child [items]="(myService.items | async)"></app-child>',
})
export class Parent {

  constructor(public myService: MyService) {
  }
}

- added brackets in the [items] assignation

Child :

@Component({
  selector: 'app-child',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: '<ul><li *ngFor="item of _items">{{item.name}}</li></ul>',
})
export class Child implements OnChanges {

  @Input() items;
  _items = [];

  constructor() {
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log('changes', changes);
    console.log(this.items);
    this._items = changes.items;
  }
}
  • change class name
  • added changeDetection strategy
  • also a good practice to prefix with app your selector
4
votes

You are almost there :-). I have just a few tips...

@Injectable()
export class MyService {

  // Post-fixing your observable variables with `$` will make it easy to see them at the first glance.
  private _items$: BehaviorSubject<any[]> = new BehaviorSubject([]);
  get items$(): Observable<any[]> {
    // BehaviorSubject is an Observable as well
    return this._items$;
    // Or if you want to be safe and not leak your abstraction, use what you did...
    return this._items$.asObservable();
  }

  list() {
    // BehaviorSubject can subscribe itself
    this.http.get('...').subscribe(this._items$);
  }
}

I would store the observable in your parent component in a field. If you do not need your service anywhere else in the component, why keep a reference.

@Component({
  selector: 'parent',
  templateUrl: '<child [items]="items$ | async"></child>',
})
export class ParentComponent {
  items$: Observable<any[]>;
  constructor(myService: MyService) {
    this.items$ = myService.items$;
  }
}

In this case you don't need the ngOnChanges since you can make your input into a property. If you don't need the logging, you can ditch the accessors altogether.

@Component({
  selector: 'child',
  templateUrl: '<ul><li *ngFor="item of items">{{item.name}}</li></ul>',
})
export class ChildComponent{

  private _items = [];

  @Input() set items(_items) {
    console.log('items', _items);
    this._items = _items;
  }
  get items() {
    return this._items;
  }
}

Your async data handling approach is fine. Concern separation into 'smart' and 'dumb' components is the way to go in my opinion.

Hope this helps a little :-)