angularrxjsobservableangular17subject

Input Subject is empty in child component Angular 17


I am trying to pass data from a parent to its child component in Angular 17. The data is retrieved from the back-end through an API that gets an array of items.

In my parent component, items$ is a subject:

items$ = Subject<Array<Item>> = new Subject();

I subscribe it to the method of my service that retrieves all items from the backend:

ngOnInit(){
    this.itemService.getItems().subscribe(this.items$);
}

In my parent.html template, this code does display that there are items:

@if(items$ | async){
"There are items!"
}@else{
"There aren't any items"
}

So I then call my child component:

@if(items$ | async){
<app-child [items$]=items$>
}

This is what my child.component.ts looks like:

export class ChildComponent implements OnInit{
@Input() items$: Observable<Array<Item>>;

ngOnInit(){
this.items$.subscribe();
}
}

An child.component.html:

@if(items$ | async){
"There are items in the child component!"
}@else{
"There aren't any items in the child component"
}

Well, it displays that there aren't any items in the child component.

I know I don't need to subscribe in the onInit() method of my child component, but I really just wanted to make sure that I am indeed subscribed to my multicast observable.

Also, interestingly, when I take my child component out of the @if condition in parent.html, the items are displaying!

Any idea why? And how can I condition the display of my child component to the presence of items?


Solution

  • You can convert the Subject to a BehaviorSubject, so that it maintains state (the array). Prefer subject when you want to trigger emissions for events (Event Bus) not holding state.

    Also you should write a callback inside the subscribe that calls the next method of the BehaviorSubject with the data from the API.

    itemService = inject(ItemService);
    items$: BehaviorSubject<Array<any> | null> =
      new BehaviorSubject<Array<any> | null>(null);
    
    ngOnInit() {
      this.itemService.getItems().subscribe((data: any) => {
        this.items$.next(data);
      });
    }
    

    Full Code:

    import { Component, inject, Injectable, Input, OnInit } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { Observable, of, Subject, BehaviorSubject } from 'rxjs';
    import { CommonModule } from '@angular/common';
    
    @Injectable({ providedIn: 'root' })
    export class ItemService {
      getItems() {
        return of([{ test: 1 }]);
      }
    }
    
    @Component({
      selector: 'app-child',
      imports: [CommonModule],
      template: `
        @if(items$ | async) {
          "There are items in the child component!"
        } @else {
          "There aren't any items in the child component"
        }
      `,
    })
    export class ChildComponent implements OnInit {
      @Input() items$!: Observable<Array<any> | null>;
    
      ngOnInit() {
        this.items$.subscribe();
      }
    }
    
    @Component({
      selector: 'app-root',
      imports: [CommonModule, ChildComponent],
      template: `
        @if(items$ | async){
          "There are items!"
        }@else{
          "There aren't any items"
        }
        <hr/>
        @if(items$ | async) {
          <app-child [items$]="items$"/>
        }
      `,
    })
    export class App {
      itemService = inject(ItemService);
      items$: BehaviorSubject<Array<any> | null> =
        new BehaviorSubject<Array<any> | null>(null);
    
      ngOnInit() {
        this.itemService.getItems().subscribe((data: any) => {
          this.items$.next(data);
        });
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo