Asked 1 month ago by CosmicResearcher773
Angular: Why are new subscriptions to NavigationService returning the initial BehaviorSubject value?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by CosmicResearcher773
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
This question already has an answer here:
Angular2 sibling component communication, component not subscribing to service
(1 answer)
Closed 12 days ago.
I have a relatively simple Angular app where I use a service to pass values between components. The problem is that when I navigate to a new route and load a component that subscribes to the service, it always returns the initial value ("Home") from the BehaviorSubject instead of the current value that was set.
The main app.component contains a menu component and a router-outlet for loading other components based on the route when the user clicks a menu item. The menu component subscribes to the service and updates the current selected menu item correctly. However, other components subscribing to the same service always see the initial value.
The menu component:
TYPESCRIPT@Component({ selector: 'app-menu', templateUrl: './menu.component.html', styleUrls: ['./menu.component.scss'], providers: [MenuDatabase, NavigationService], standalone: false }) export class MenuComponent implements OnInit { @Input() treeControl = new NestedTreeControl<MenuItem>(node => { return (node.models) ? node.models : node.actions; }); public activeItem: MenuItem | undefined; public versionList: Version[] = []; public menuItemList: MenuItem[] = []; public currentVersion: string = ''; public isSearching: boolean = false; public searchForm = new UntypedFormGroup({ search: new UntypedFormControl('') }); hasChild = (_: number, node: MenuItem) => (!!node.models && node.models.length > 0) || (!!node.actions && node.actions.length > 0); dataSource = new MatTreeNestedDataSource<MenuItem>(); constructor( public router: Router, public menuService: MenuService, private navigationService: NavigationService, private database: MenuDatabase, ){ navigationService.activeMenuItem$.subscribe( item => { this.activeItem = item; } ) } ngOnInit(): void { } navigate(menuItem: MenuItem) { if(menuItem.models) this.router.navigate([`${this.currentVersion}/module/${menuItem.code}`], { state: { ep: menuItem } }); if(menuItem.actions) this.router.navigate([`${this.currentVersion}/model/${menuItem.code}`], { state: { ep: menuItem } }); if(!menuItem.actions && !menuItem.models) this.router.navigate([`${this.currentVersion}/action/${menuItem.code}`], { state: { ep: menuItem } }); this.navigationService.setActiveMenuItem(menuItem) } }
NavigationService:
TYPESCRIPT@Injectable({ providedIn: 'root' }) export class NavigationService { private defaultItem = { title: "Home", code: "home" } // Observable string sources private activeMenuItemSource = new BehaviorSubject<MenuItem>(this.defaultItem); // Observable string streams activeMenuItem$ = this.activeMenuItemSource.asObservable(); constructor(){ } // Service message commands setActiveMenuItem(activeItem: MenuItem) { console.log("from: " + this.activeMenuItemSource.getValue().title + " | to: " + activeItem.title) this.activeMenuItemSource.next(activeItem); } }
The console.log in the service shows the expected value change when setActiveMenuItem() is called from the menu component.
Component loaded upon navigation (ActionPageComponent):
TYPESCRIPT@Component({ selector: 'app-action-page', templateUrl: './action-page.component.html', styleUrls: ['./action-page.component.scss'], standalone: false }) export class ActionPageComponent implements OnInit { @Input() treeControl = new NestedTreeControl<MenuItem>(node => { return (node.models) ? node.models : node.actions; }); @Input() menuItem: MenuItem; constructor( private activatedRoute: ActivatedRoute, private actionService: ActionService, private navigationService: NavigationService ) { }; ngOnInit(): void { this.activatedRoute.url.subscribe(seg => { this.version = seg[0].path || ''; this.level = seg[1].path || ''; this.itemCode = seg[2].path || ''; this.actionService.getAction(this.itemCode) .subscribe(data => { this.item = data }) }); this.navigationService.activeMenuItem$.subscribe( item => { console.log(item) } ) } }
The console log in ActionPageComponent always shows "Home" (the initial default value).
How can I get components loaded via router-outlet to subscribe to the NavigationService and receive the current value instead of the initial default?
Comments 0
•Answers 3
•Views 50
Answer by TychoAI Agent • 1 month ago
The issue is that you're creating multiple instances of NavigationService. In your MenuComponent you added NavigationService to its providers array:
TYPESCRIPT@Component({ selector: 'app-menu', templateUrl: './menu.component.html', styleUrls: ['./menu.component.scss'], providers: [MenuDatabase, NavigationService], standalone: false })
This makes Angular create a new instance specifically for the menu component. The router-outlet’s components (like ActionPageComponent) which inject NavigationService are receiving the global instance (provided in 'root'). This means they subscribe to a different BehaviorSubject than the one modified by MenuComponent.
To fix this, remove NavigationService from the MenuComponent providers array so that all components share the same instance from the global injector:
TYPESCRIPT@Component({ selector: 'app-menu', templateUrl: './menu.component.html', styleUrls: ['./menu.component.scss'], providers: [MenuDatabase], // Remove NavigationService here. standalone: false }) export class MenuComponent implements OnInit { // ... rest of your code }
Now, both the MenuComponent and ActionPageComponent will subscribe to the same service instance, and the router-loaded component will receive the current active menu item value instead of the initial default.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by MeteoricTracker760 • 1 month ago
You should set the BehaviorSubject value and then trigger the navigation. If that also does not work, as a workaround, you can wrap the navigation code inside a setTimeout
.
TYPESCRIPTnavigate(menuItem: MenuItem) { this.navigationService.setActiveMenuItem(menuItem); // <- changed here! // setTimeout(() => { if(menuItem.models) { this.router.navigate([`${this.currentVersion}/module/${menuItem.code}`], { state: { ep: menuItem } }); } else if(menuItem.actions) { this.router.navigate([`${this.currentVersion}/model/${menuItem.code}`], { state: { ep: menuItem } }); } else if(!menuItem.actions && !menuItem.models) { this.router.navigate([`${this.currentVersion}/action/${menuItem.code}`], { state: { ep: menuItem } }); } // }); }
No comments yet.
Answer by NeutronCommander267 • 1 month ago
It looks like you are injecting multiple instances of your navigation service.
You've decorated your service with providedIn: 'root'
, which means it will be created at the root level of the application. But it looks like your MenuComponent
is providing a new instance of the service (since you provided it in the providers array).
Component will inject the first instance of the service it finds coming up the tree, meaning that your MenuComponent
and all the components placed inside it will use the instance provided by it.
On the other hand, all the components that are not placed inside your MenuComponent
will use the instance that is provided in the root (or somewhere along the way, if some other component provides it).
So odds are, your setActiveMenuItem
method is being called on one instance, but your component is observing the other instance, so those are two separate BehaviorSubject
s.
You probably want it to be provided in the root only, so removing it from the providers array of your MenuComponent
is the way to go.
No comments yet.
No comments yet.