Search code examples
angularrxjsbehaviorsubject

How to get BehaviourSubject value in other component if I type the component url directly?


Let's simplify the question. Say the angular application only has a few components. In app component html,

<app-nav></app-nav>
<router-outlet></router-outlet>

The ts file we inject a service to get navigation menu. The service contains a BehaviourSubject to handle menu items.

@Injectable({
     providendIn: 'root'
})
export class AppService {
    public items = new BehaviourSubject<Item[]>([]);
    public getMenu() {
         return this.http.get<Item[]>(this.url);
    }
    public getItems() {
        return this.items.value;
    }
}

In app component ts file.

ngOnInit() {
   this.service.getMenu().subscribe(
      res => {
          this.items = res;
          this.service.items.next(this.items);
      });
}

So far so good. Then in my nav component I can receive the items.

export class NavComponent implements OnInit {
    items: any;
    constructor(private service: AppService){}
    ngOnInit() {
          this.items = this.service.getItems();
    }
}

Then in Nav html I have

   <span *ngFor="let item of items">
     <div>{{item.name}}</div>
   </span>

So the normal step is to go to http://localhost:4200 then navigation bar is displayed correctly. However if I type the url of another component directly. Say

http://localhost:4200/app/one

It seems the page loads component one first. The navigation bar is rendered before get the menu items. Actually I log the items in component one, it is empty. Eventually it also goes to app component but it is too late.

So my question is that I want to get menu items in the very first place. Whatever I typed any url I can always get the navbar menu items and rendered correctly. I doubt that I used BehaviouSubject wrongly. Any hint?


Solution

  • The problem you are having is that your nav component only sets the value of items one time. Since your behavior subject has a default value of empty array, when OneComponent.ngOnInit calls service.getItems() it receives an empty array and that value is never updated on further emissions.

    There is a question here that explores using BehaviorSubject.value appropriately. I would discourage use of .value in your case.

    It actually makes the code much more complicated, and is breaking the "observability" of the data flow.

    Since NavComponent uses AppService directly, AppComponent doesn't need to be involved at all! Just let NavComponent subscribe to items directly.

    Also, it seems unnecessarily complex to have the AppService call a service method to get the items, then another service method to have the service push the new value out. It would be much simpler if the service was responsible for pushing out the newest values and consumers of the service simply subscribe to the observable.

    service:

    export class AppService {
      private items = new BehaviorSubject<Item[]>([]);
      public items$ = this.items.asObservable();
    
      public getMenu() {
        return http.get().pipe(
          tap(items => this.items.next(items))
        );
      }
    }
    

    The code above allows consumers to simply subscribe to items$ and receive the latest value automatically. Whenever getMenu() is called, it will push the newest value through the subject.

    The NavComponent can use this directly, and the AppComponent doesn't need to do anything:

    nav component:

    export class NavComponent {
      items$ = this.service.items$;
    
      constructor(private service: AppService){ }
    
      ngOnInit() {
        this.service.getMenu().subscribe();
      }
    }
    
    <span *ngFor="let item of items$ | async">
      <div>{{ item.name }}</div>
    </span>
    

    app component:

    export class AppComponent  {
    
    }
    

    LOL, nothing in the app component. Here's a working StackBlitz

    So, going back to your original question:

    How to get BehaviorSubject value in other component if I type the component url directly?

    The overall idea is to reference the BehaviorSubject as an observable, so you always receive the most recent value; don't use .value to pull out a single value at one moment in time.