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?
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.