angular: Parameterized routes do not cause component and router links to update
When using parameterized routes, changes to the parameter are correctly recognized using the observable on the activated route, but router links that are on the component’s view are not updated. This appears to be a bug.
See a plunker with an example here.
In that example, I have a ContainerComponent at the root which takes an id parameter and displays a child router outlet. The child routes are available as router links on the container component:
template: `
<p>Container {{id}}</p>
<a [routerLink]="['foo']" routerLinkActive="active">foo</a>
<a [routerLink]="['bar']" routerLinkActive="active">bar</a>
<hr />
<router-outlet></router-outlet>
`,
Assuming I start at the URL /example1/foo, I can then use the router links in the parent component to switch between ids. For example, I can go to /example2/foo by clicking the “Example 2” link. On that route, the ContainerComponent correctly recognizes the change of the id parameter and updates the view to display that id. The links in the ContainerComponent however still link to /example1/foo and /example1/bar respectively. So the change of the active route to /example2/ was never recognized there.
So it seems that the relative routes do not take route changes into account. They are rendered once at the very beginning. This appears to be because the RouterLink directive only uses the ActivatedRoute that’s injected at construction.
The RouterLinkActive directive works differently by listening to router events and always using the current URL.
The RouterLink directive could obviously be changed to listen to those events too but I personally have a similar problem with my components in my application too. Because the v3 component router actively reuses components, switching between two different routes which happen to use the same components will not cause those components to go through the lifecycle events. As such, initialization that would happen in ngOnInit only happens for the very first route.
To properly update the component, I would have to actually listen to all router events (since you can only listen to all router events) and figure out when the current component would be affected and then perform my own lifecycle—all manually.
This all feels very wrong to me, that I have to take care of so much things and listen to so many low-level events when I just want to use the router by configuration.
- Angular version: 2.0.0-rc.4
- Router version: 3.0.0-beta.2
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 22
- Comments: 28 (9 by maintainers)
@DzmitryShylovich Oh, sorry I never got back to you about this.
Implementing a custom reuse strategy seems to be what I was looking for here. That way, I can disable the router’s component reusing completely (or even use custom logic to do it selectively) and avoid the problems I have.
I’m not completely happy with it since it still leaves the underlying problem inside the default case (I am still of the opinion that this is an odd default behavior for the router), but at least we have an actual way to get around it now (unlike at 2.0 release). I’m glad that this made it into the release eventually.
The option 1 to subscribe to the router events is not really a solution in my eyes as it still moves the responsibility to reset the state to components that should not have any knowledge about their locality within the router, but I get that this made sense as a solution to the RouterLink problematic as fixed in f65ebec3edf770aadb642c0b4f4db9e49060b30a.
But yeah, I’m happy with the custom reuse strategy and this issue can be closed I guess. Thank you for your help!
For those interested or running into the situation themselves: To disable the router reusing, you basically have to implement a custom
RouteReuseStrategy. You can use theDefaultRouteReuseStrategyas a base and then just change theshouldReuseRouteto returnfalsewhenever you want to disable the route reusing.Note that you cannot just return
falsepermanently here as this will break the router completely. In cases wherecurr.routeConfigandfuture.routeConfigarenull, you have to returntrueinstead.So basically, assuming no custom logic to actually reuse any routes, your implementation would look like this:
You can then provide that class as a
RouteReuseStrategyusing{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }in your NgModule and everything should work just fine.You can see this in action in this Plunker that builds upon the previously linked example.
Hi, I have the same issue ! The configuration is the same.
+1
I just ran into this also.
Ok, thx.
this is because component is reused as described here https://angular.io/docs/ts/latest/guide/router.html#!#reuse
So u have several options:
route.paramsand reset component state on updates.It’s not a problem for the
ContainerComponentsince it’s actively listening to the parameter change anyway. It is already fully aware of the router presence as it gets the parameter value, so it knows that it has to handle changes and can properly handle it.However, it’s a problem further down the line. Suppose
FooComponentin my example requires a service to be injected, so it has aproviderset:Now, this component is completely unaware of the router. Following separation of concerns it does not need nor should know in what context it is being used. It just knows that it requires a
MyServicewhen its being constructed and everything is configured so that’s the case.Now, when switching between
fooandbarroutes, this is no problem. The component gets properly created, a service instance is created and injected. However, when switching between variousfooroutes, e.g./example1/fooand/example2/foonot only is theContainerComponentbeing reused (as expected), but also theFooComponent(and everything that’s inside). So theFooComponentdoes not realize it now lives in a completely separate context. It does not know that the route changed, and that it’s just being used because some router decided to.Although we are in a completely different context (
example2instead ofexample1), theFooComponentis not able to realize that without actively listening to router events. And even if it did, it wouldn’t be able to receive a new and cleanMyServiceinstance. It would be required to somehow reset the service. This would also require the service to be able to do so.So just because the router is reusing the components, we are adding a huge complexity to the application that all components and dependencies below a reused component need to be aware and capable of possible reuse as well.
@pantonis and @poke this worked for me:
app.component.ts (setting up method for scroll to top of page)
import { Component, Renderer, OnInit } from '@angular/core'; import { Router } from '@angular/router'; ... export class AppComponent { constructor( private renderer: Renderer ) { } ngOnInit() { } // on page reload, scroll to top of window onDeactivate() { this.renderer.setElementProperty(document.body, "scrollTop", 0); } }app.component.html
NOTE: Added “scroll to top” functionality because it’s largely requested and coupled with param change
app.routes.ts (setting up route parameter that will be updated)
... import { ProductItemComponent } from './components/product-item/product-item.component.ts ... export const routes: routes = [ ... { path: 'product/:id', component: ProductItemComponent } ... ];product-item.component.ts (where param id subscription is initiated)
import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { ProductService } from '../../services/product.service'; import { Item } from '../../models/Item'; ... export class ProductItemComponent implements OnInit { ... id: string; item: Item; constructor( public productService: ProductService, public route: ActivatedRoute, public router: Router ) { // reset item object based on params id change this.route.params .subscribe(params => { this.id = params['id']; // reuse route, state refreshes, page scrolls to top this.router.routeReuseStrategy.shouldReuseRoute = function() { return false; } // get item object this.productService.getItem(this.id) .subscribe(item => { this.item = item; }); }); } ngOnInit() { // get item id from url this.id = this.route.snapshotparams['id']; // get item object this.productService.getItem(this.id) .subscribe(item => { this.item = item; }); } }NOTE: There are two calls for the item object. The first one (in ngOnInit), is called as the page is constructed. The second one (in constructor) is called as param change is recognized.
@poke there is a comment from Victor on component reuse status here: https://github.com/angular/angular/issues/7757#issuecomment-236737846
Please use the add reaction feature on the initial comment instead +1 or me too