Hello
Hello

Reputation: 816

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?

Upvotes: 0

Views: 5436

Answers (3)

BizzyBob
BizzyBob

Reputation: 14750

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.

Upvotes: 1

Loop
Loop

Reputation: 510

So you want the Menu Bar rendered on any page immediately? If that's true, I think putting your initializing code into the app.component.ts is not quite what you want.

I would say that using APP_INITIALIZER to load menu items before your application starts might be a better choice. See e.g. Angular: How to correctly implement APP_INITIALIZER

Upvotes: 0

Daniel
Daniel

Reputation: 31

You need to subscribe the items BehaviourSubject in component one.

Here is my solution:

   items: any;
   constructor(private service: AppService){}
   ngOnInit() {
      this.service.items.subcribe(items => this.items = items);
   }

Because the Component One was initialized before the get request done. At this time, the BehaviourSubject didn't receive any new value so it emitted the seed value (an empty array). So I think subscribe() will work.

Upvotes: 0

Related Questions