Search code examples
angularobservablengrx

Angular view ngFor not displaying observable collection after it has been updated in ngOnInit


I have an Angular 9 application using NGRX that for the most part uses observables/subjects and the assync pipe.

I have a navbar component that has the following HTML:

<ul class="navbar-nav">
    <li class="nav-item" *ngFor="let menuItem of navbarMenuItems | async">
      <a id="{{ menuItem.id }}" [routerLink]="menuItem.routerLink" class="nav-link">
        <span><i class="icon" [className]="menuItem.iconClass"></i>{{ menuItem.translationKey | translate }}</span>
    </a>
  </li>
</ul> 

The navbarMenuItems is an observable array that I attach in my component ngOnInit method:

this.navbarMenuItems = this.navbarService.getNavbarMenuItems();

The NavbarService looks like this:

@Injectable({
  providedIn: "root"
})
export class NavbarService {

  public navbarMenuItems = new Subject<NavbarActionModel[]>();

  constructor(
  ) { }

  updateNavbarMenuItems(items: NavbarActionModel[]) {
    this.navbarMenuItems.next(items);
  }

  getNavbarMenuItems(): Observable<any> {
    return this.navbarMenuItems.asObservable();
  }
}

Everything works perfect when calling the NavbarService updateNavbarMenuItems() method from any other component (i.e. the navbar updates and shows the actions that you send it).

However, if I call the updateNavbarMenuItems() in the NavbarComponent (i.e. the one with the HTML) ngOnInit method the HTML doesn't update. BUT if I call the update method in the ngAfterViewInit() method they do appear.

A colleague of mine helped me fix the issue and told me it's because of a race condition where the *ngFor isn't rendered in time, which seems to be right. However, this seems really strange to me and I feel like I must be doing something else wrong and there is another bug in my code/best practice not followed if anyone can help?


Solution

  • Subject is not returning any value on Subscription, it triggers only new value emitted by .next(value).

    BehaviorSubject is returning also last emitted value on each new Subscription.

    In this case, you should use BehaviorSubject to be sure that value will be emitted a new time when async pipe will subscribe to observable.

    Check this Stackblitz demo.

    You can see that if we simulate a delay (with setTimeout or HTTP request...), the value will be emitted after view is initialized, hence async pipe already subscribed.

    In case of BehaviorSubject, when view is initialized, navbarMenuItems | async will receive the current value (already emitted during view init), and then explicitly mark view as dirty (need update).

    UPDATE (additional explanation) :

    OnInit is called just at the beginning of rendering process. Later Pipe directive is binded, and transform method is called. During this first called, AsyncPipe subscribes to Observable.

    Hence, AsyncPipe subscribes to the Observable after OnInit is executed.

    But in our case, value of Observable was already emitted one time.