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)

Most upvoted comments

@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 the DefaultRouteReuseStrategy as a base and then just change the shouldReuseRoute to return false whenever you want to disable the route reusing.

Note that you cannot just return false permanently here as this will break the router completely. In cases where curr.routeConfig and future.routeConfig are null, you have to return true instead.

So basically, assuming no custom logic to actually reuse any routes, your implementation would look like this:

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
  store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
  shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return null; }
  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return curr.routeConfig === null && future.routeConfig === null;
  }
}

You can then provide that class as a RouteReuseStrategy using { 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.

all have the same input field.

this is because component is reused as described here https://angular.io/docs/ts/latest/guide/router.html#!#reuse

So u have several options:

  1. subscribe to route.params and reset component state on updates.
  2. u can implement custom reuse strategy and always re-create the component

In this case your ContainerComponent has no need to be reloaded, since the only thing changing is your params, which you get notified of.

It’s not a problem for the ContainerComponent since 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 FooComponent in my example requires a service to be injected, so it has a provider set:

@Component({
    template: `<p>Foo</p>`,
    providers: [MyService]
})
export class FooComponent {
    constructor(svc: MyService) {}
}

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 MyService when its being constructed and everything is configured so that’s the case.

Now, when switching between foo and bar routes, this is no problem. The component gets properly created, a service instance is created and injected. However, when switching between various foo routes, e.g. /example1/foo and /example2/foo not only is the ContainerComponent being reused (as expected), but also the FooComponent (and everything that’s inside). So the FooComponent does 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 (example2 instead of example1), the FooComponent is 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 clean MyService instance. 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

< app-navbar ></ app-navbar >
< router-outlet (deactivate)="onDeactivate()" ></ router-outlet >
< footer ></ footer >

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