angular: Cannot reattach ActivatedRouteSnapshot created from a different route

I’m submitting a … (check one with “x”)

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Navigate to a route that has child routes. Navigate to one of the child routes. Navigate back to top level route. Navigate to any other root. (this is where you receive the error)

Expected behavior

Navigate to a route that has child routes. Navigate to one of the child routes. Navigate back to top level route. Navigate to any other root. (without receiving the error)

Minimal reproduction of the problem with instructions

Step1 1

Step2 2

Step3 3

Step4 4

Error error

https://plnkr.co/edit/GuQuWnW2GsfnBOVyWQRh?p=preview

What is the motivation / use case for changing the behavior? To fix the bug

Please tell us about your environment: ASP.Net Core

  • Angular version: 3.1.1

  • Angular/router Version: 3.3.1

  • Browser: [Chrome]

  • Language: [TypeScript 1.0.3.0 | ES6 | ES5]

  • Node (for AoT issues): node v6.9.2

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 31
  • Comments: 65 (6 by maintainers)

Most upvoted comments

Removing “docs” label because the solution is not something we intend to put in Angular docs. The problem is obscure and not of general interest. This kind of thing would be better in Stack Overflow.

Is it possible for the ng2 router team to publish a correct RouteReuseStrategy that would work properly for nested routes and avoid this defect? Currently this is not available anywhere - including SO or any other website.

Since this is a fundamental underpinning of how one would design their application (for those who want to avoid destructing the components), it is not possible to even start developing an Angular2 application without a correct (some kind of) AttachDetachReuseStrategy.

There seems to be no communication on this since Jan. Please help.

@wardbell please explain how the “problem is obscure and not of general interest”?

I don’t understand why this is an obscure requirement. If you have a list and a detail/view page you probably want to preserve search filters, sort, collapsible options, checkboxes, pagination etc when you go back.

So how to preserve state ?

Oner solution is to do it manually, save everything in a service, localstorage, cookie, query params whatever but that is sooo messy… saving so many state and setting them back is hard to maintain. Any new addition will require taking care of this new state. Not good.

Another solution is to have a router-outlet inside a parent component which holds the state. Not a big fan of this because states that should be the concerns of a child component are now moved up and the state still goes if you route outside of the parent component route because then the parent get destroyed. Not good.

I’m wondering if something like class-transformer can make it easier to serialize component and restore them. But still, storing an entire object and rebuilding it just after a navigation ?

RouteReuseStrategy: this is so clean ! But it throws this error and I cannot find many resources on how to properly implement it.

So the initial question of how to preserve state is still open for me.

lol, why is this closed ? This issue is still not resolved here or documented in the Angular-Docs. There is no info how to securly use the ReuseStrategy and in more complex scenarios with lazy loaded child routes i get lost…

The linked issue when you closed this, has nothing to do with this. It is still not an answer. We need professional examples in the documentation on how to use the RouterReuseStrategy also in complex scenarios…

Please reopen. It is a bug.

From what I can tell the main issue is that angular is internally comparing the entire detached tree for a given route and if ALL nested routes are not the same then the entire route fails.

In terms of the OP:

  • When you navigate back to the top level route (after editing a person), the stored route is the entire url tree starting at the ‘person/:id’ level. (essentially ‘person/:id/edit’)
  • When you navigate back to the ‘person/:id’ via a “view” button the new route is ‘person/:id/view’, which fails angular’s internal check (setFutureSnapshotsOfActivatedRoutes) since the child routes for ‘person/:id’ are different

My solution is to update/override any redirects when I store a route

export class CustomRouteReuseStrategy extends RouteReuseStrategy {
  handlers: {[path:string]:DetachedRouteHandle} = {};

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    //Avoid second call to getter
    let config: Route = route.routeConfig;
    //Don't store lazy loaded routes
    return config && !config.loadChildren;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    let path: string = this.getRoutePath(route);
    this.handlers[path] = handle;
    /*
      This is where we circumvent the error.
      Detached route includes nested routes, which causes error when parent route does not include the same nested routes
      To prevent this, whenever a parent route is stored, we change/add a redirect route to the current child route
    */
    let config: Route = route.routeConfig;
    if(config) {
      let childRoute: ActivatedRouteSnapshot = route.firstChild;
      let futureRedirectTo = childRoute ? childRoute.url.map(function(urlSegment) {
        return urlSegment.path;
      }).join('/') : '';
      let childRouteConfigs: Route[] = config.children;
      if(childRouteConfigs) {
        let redirectConfigIndex: number;
        let redirectConfig: Route = childRouteConfigs.find(function(childRouteConfig, index) {
          if(childRouteConfig.path === '' && !!childRouteConfig.redirectTo) {
            redirectConfigIndex = index;
            return true;
          }
          return false;
        });
        //Redirect route exists
        if(redirectConfig) {
          if(futureRedirectTo !== '') {
            //Current activated route has child routes, update redirectTo
            redirectConfig.redirectTo = futureRedirectTo;
          } else {
            //Current activated route has no child routes, remove the redirect (otherwise retrieval will always fail for this route)
            childRouteConfigs.splice(redirectConfigIndex, 1);
          }
        } else if(futureRedirectTo !== '') {
          childRouteConfigs.push({
            path: '',
            redirectTo: futureRedirectTo,
            pathMatch: 'full'
          });
        }
      }
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!this.handlers[this.getRoutePath(route)];
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    let config: Route = route.routeConfig;
    //We don't store lazy loaded routes, so don't even bother trying to retrieve them
    if(!config || config.loadChildren) {
      return false;
    }
    return this.handlers[this.getRoutePath(route)];
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }

  getRoutePath(route: ActivatedRouteSnapshot): string {
    let namedOutletCount: number = 0;
    return route.pathFromRoot.reduce((path, route) => {
      let config: Route = route.routeConfig;
      if(config) {
        if(config.outlet && config.outlet !== PRIMARY_OUTLET) {
          path += `(${config.outlet}:`;
          namedOutletCount++;
        } else {
          path += '/';
        }
        return path += config.path
      }
      return path;
    }, '') + (namedOutletCount ? new Array(namedOutletCount + 1).join(')') : '');
  }
}

I know this doesn’t help everybody (or the OP for that matter), but it is sufficient for my use case and may be for some of you

Note: My actual RouteReuseStrategy is slightly more complicated (I extended the Route interface to provide additional configuration, allowing for each route to be handled differently).

@pkozlowski-opensource can you please have a look at this issue or reopen it?

My workaround for this problem was to make my retrieve function check for loadChildren on the routeConfig like so:

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) return null;
        if (route.routeConfig.loadChildren) return null;
        return this.handlers[route.routeConfig.path];
    }

Alright, guys, here’s the solution:

@wardbell is absolutely right, the problem is obscure and you need to think about it in more detail.

Let’s assume we have following routes:

    {path: 'settings', component: SettingsComponent, children: []},
    {path: 'project', component: ProjectListComponent, children: [
        {path: ':id', component: ProjectComponent},
    ]},
  • You open /project/1. So you tell Angular to load a route which contains 2 routes (the actual one and one children).
  • Then open /settings. What happens now is interesting:
    • Angular triggers RouteReuseStrategy::store for route project, which contains the very moment one children in its handle. So you save that including the children (as you can’t and shouldn’t modify the handle)
  • You go back to /project (note: not the one from 1.). Now Angular wants from your ReuseStrategy the handle for project. You return the one with 1 children (since you have stored that, remember?)
    • Angular wants now to load a view containing only one route with a handle that contains two routes. What to do? Well, angular decides to throw an error, which is fine to me, as the behavior is not very obvious. Should angular instead remove that children route and destroy its Component? I don’t know. Why don’t you return a correct one?

So what’s the issue here? The issue is that you return a handle that is not the correct one. You return a handle containing a children although angular wanted the handle for /project (no children involved here).

The solution is simple: Store the handle not based on route name, not even on url, but on the whole state with children. Usually you store it like this:

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
   this.myStore[route.routeConfig.path] = handle;
}

This is not enough. Also using the full url including all parents path is not enough. You need a different key to define the current state of ActivatedRouteSnapshot, which is perfectly defined by its full url PLUS its childrens.

Additions:

  1. Memory Leak

once you store a handle in your reuse strategy, angular is never destroying related Components. You need to call destroy() on them once you remove them out of your cache. However, since DetachedRouteHandle is defined as {} you need currently a little hack.

  1. Multiple caches per resolved url

When you store the handle based on a simple key path of your config or the full path including all parents, then you store for dynamic routes (like /project/:id) only ever the latest. My following implementation allows you to define for example to hold the last 5 detail views of /project/:id.

Here is an implementation that should cover all use-cases:

import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from "@angular/router";
import {ComponentRef} from "@angular/core";

interface RouteStates {
    max: number;
    handles: {[handleKey: string]: DetachedRouteHandle};
    handleKeys: string[];
}

function getResolvedUrl(route: ActivatedRouteSnapshot): string {
    return route.pathFromRoot
        .map(v => v.url.map(segment => segment.toString()).join('/'))
        .join('/');
}

function getConfiguredUrl(route: ActivatedRouteSnapshot): string {
    return '/' + route.pathFromRoot
        .filter(v => v.routeConfig)
        .map(v => v.routeConfig!.path)
        .join('/');
}

export class ReuseStrategy implements RouteReuseStrategy {
    private routes: {[routePath: string]: RouteStates } = {
        '/project': {max: 1, handles: {}, handleKeys: []},
        '/project/:id': {max: 5, handles: {}, handleKeys: []}
    };

    /** Determines if this route (and its subtree) should be detached to be reused later */
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return !!this.routes[getConfiguredUrl(route)];
    }

    private getStoreKey(route: ActivatedRouteSnapshot) {
        const baseUrl = getResolvedUrl(route);

        //this works, as ActivatedRouteSnapshot has only every one children ActivatedRouteSnapshot
        //as you can't have more since urls like `/project/1,2` where you'd want to display 1 and 2 project at the
        //same time
        const childrenParts = [];
        let deepestChild = route;
        while (deepestChild.firstChild) {
            deepestChild = deepestChild.firstChild;
            childrenParts.push(deepestChild.url.join('/'));
        }

        //it's important to separate baseUrl with childrenParts so we don't have collisions.
        return baseUrl + '////' + childrenParts.join('/');
    }

    /**
     * Stores the detached route.
     *
     * Storing a `null` value should erase the previously stored value.
     */
    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
        if (route.routeConfig) {
            const config = this.routes[getConfiguredUrl(route)];
            if (config) {
                const storeKey = this.getStoreKey(route);
                if (handle) {
                    if (!config.handles[storeKey]) {
                        //add new handle
                        if (config.handleKeys.length >= config.max) {
                            const oldestUrl = config.handleKeys[0];
                            config.handleKeys.splice(0, 1);

                            //this is important to work around memory leaks, as Angular will never destroy the Component
                            //on its own once it got stored in our router strategy.
                            const oldHandle = config.handles[oldestUrl] as { componentRef: ComponentRef<any> };
                            oldHandle.componentRef.destroy();

                            delete config.handles[oldestUrl];
                        }
                        config.handles[storeKey] = handle;
                        config.handleKeys.push(storeKey);
                    }
                } else {
                    //we do not delete old handles on request, as we define when the handle dies
                }
            }
        }
    }

    /** Determines if this route (and its subtree) should be reattached */
    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        if (route.routeConfig) {
            const config = this.routes[getConfiguredUrl(route)];

            if (config) {
                const storeKey = this.getStoreKey(route);
                return !!config.handles[storeKey];
            }
        }

        return false;
    }

    /** Retrieves the previously stored route */
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
        if (route.routeConfig) {
            const config = this.routes[getConfiguredUrl(route)];

            if (config) {
                const storeKey = this.getStoreKey(route);
                return config.handles[storeKey];
            }
        }

        return null;
    }

    /** Determines if `curr` route should be reused */
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return getResolvedUrl(future) === getResolvedUrl(curr) && future.routeConfig === curr.routeConfig;
    }
}

Note: To destroy elements we used above

const oldHandle = config.handles[oldestUrl] as { componentRef: ComponentRef<any> };
oldHandle.componentRef.destroy()

which can obviously break at any time Angular going forward with new releases. So take care of that when you upgrade. When Angular decides to build a contract on DetachedRouteHandle then we can drop that hack.

Thanks for @DanRibbens 's workaround.Base on it,I find my workaround make my app’s RouteReuseStrategy works fine.

As DanRibbens says,make retrieve function check for loadChildren:

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) return null;
        if (route.routeConfig.loadChildren) return null;
        return this.handlers[route.routeConfig.path];
    }

then I make shouldDetach function also check for loadChildren to prevent RouteReuseStrategy save wrong ActivatedRouteSnapshot:

shouldDetach(route: ActivatedRouteSnapshot): boolean {
        if (!route.routeConfig || route.routeConfig.loadChildren) {
            return false;
        }
        return true;
    }

This is router config from plunkr:

    {path: '', redirectTo: 'search', pathMatch: 'full'},
    {path: 'search', component: SearchComponent},
    {
      path: 'person/:id', component: PersonComponent,
      children:[
        { path: '', redirectTo: 'view', pathMatch: 'full' },
        { path: 'view', component: ViewPersonComponent },
        { path: 'edit', component: EditPersonComponent }
        ]
    }

Custom reuse strategy uses route.routeConfig.path as a key in local storage, but it does not work when you have child routes, in this case you can see that there are two child routes for person/:id: View and Edit. Storage entry for person/:id gets overwritten with person/:id/Edit and when person/:id is retrieved last time it actually returns person/:id/Edit route, but expected route is person/:id/View.

The question is how should we choose key for routes when implementing custom reuse strategy? route.routeConfig.path is not suitable because of the reasons stated above, we need unique route id for any given route. Another question is why we get person/:id here at all? It is in the middle of the path.

I too have same issue with routes containing child routes when using reuse strategy. I get the error when I try to navigate back to a (stored) route with child routes.

Thank you @dmitrimaltsev for the workaround. I made a demo of it (including a custom router link directive like you mentioned) here on stackblitz

I slightly modified the reuse strategy to update the redirects when a route is reused. Otherwise navigating e.g. Parent/Child1 -> Sibling -> Parent/Child1 -> Parent/Child2 -> Parent will result in a redirect to Parent/Child1.

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    let ret = future.routeConfig === curr.routeConfig;
    if (ret) {
      this.addRedirectsRecursively(future); // update redirects
    }
    return ret;
  }

Here is the basic custom router link implementation:

import { Directive, ElementRef, OnInit, Input } from '@angular/core'
import { Router } from '@angular/router'

@Directive({
  selector: '[customRouterLink]'
})
export class CustomRouterLink implements OnInit {

  @Input() customRouterLink: string = '';
  private pathParts: string[];

  constructor(
    private elementRef: ElementRef,
    private router: Router) {}

  ngOnInit() {
    this.pathParts = this.customRouterLink.split("/");
    this.elementRef.nativeElement.addEventListener('click', this.activate.bind(this));
  }

  activate() {
    if (this.pathParts.length) {
      let p = this.pathParts[0];
      // console.log("navigating to " + p);
      let promise: Promise<any> = this.router.navigateByUrl(p);

      for (let i = 1; i < this.pathParts.length; i++) {
        p = p + "/" + this.pathParts[i];
        promise = promise.then(() => {
          // console.log("navigating to " + p);
          this.router.navigateByUrl(p);
        });
      }        
    }
  }
}

Note that this still does not work if you have parent routes that do not have default child routes.

for nested route I just ignored the loadChildren setting to prevent errors.

and then I joined all the urls from pathFromRoot to prevent duplicate keys, we might use same key for the leaf route after all, I use a lot of ‘list’ in my project

further more, not all the routes need buffering states, for example, the best behavior for detail page is to init it each time. so I put a configuration in my routeConfig.data named ‘reuse’, so I will determine which page is buffered by route setting.

here’s my code: ` export class ConfigurableRouteReuseStrategy implements RouteReuseStrategy { handlers: { [key: string]: DetachedRouteHandle } = {};

calcKey(route: ActivatedRouteSnapshot) {
    const url = route.pathFromRoot.map(x => x.url.map(u => u.path).join('/')).join(';');
    // console.debug('calcKey url: ' + url);
    if (!url.length) return undefined;
    return url;
}

shouldDetach(route: ActivatedRouteSnapshot): boolean {
    // console.debug('CustomReuseStrategy:shouldDetach', route);
    if (!route.routeConfig) return false;
    if (route.routeConfig.loadChildren) return false;
    if (route.routeConfig.data && route.routeConfig.data.reuse) {
        return true;
    }
    return false;
}

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    // console.debug('CustomReuseStrategy:store', route, handle);
    if (!route.routeConfig) return;
    if (route.routeConfig.loadChildren) return;
    if (route.routeConfig.data && route.routeConfig.data.reuse) {
        const key = this.calcKey(route);
        if (key) this.handlers[key] = handle;
    }
}

shouldAttach(route: ActivatedRouteSnapshot): boolean {
    // console.debug('CustomReuseStrategy:shouldAttach', route);
    if (!route.routeConfig) return false;
    if (route.routeConfig.loadChildren) return false;
    if (route.routeConfig.data && route.routeConfig.data.reuse) {
        const key = this.calcKey(route);
        if (key) return !!this.handlers[key];
    }
    return false;
}

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    // console.debug('CustomReuseStrategy:retrieve', route);
    if (!route.routeConfig) return null;
    if (route.routeConfig.loadChildren) return null;
    if (route.routeConfig.data && route.routeConfig.data.reuse) {
        const key = this.calcKey(route);
        if (key) return this.handlers[key] || null;
    }
    return null;
}

shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    // console.debug('CustomReuseStrategy:shouldReuseRoute', future, curr);
    return future.routeConfig === curr.routeConfig;
}

} `

This still seems to be a problem with the router even if the docs team doesn’t want to add the details of a workaround to the angular docs site?

I agree with @christianacca in that it would be nice to hear from the Angular team on their plans for the experimental RouteReuseStrategy. Many people are trying to make use of it to solve user pain points in their app. Are there plans to move this API to stable? Improve it? Or deprecate it?

I have also posted the issue on SO

I faced this problem, too. I modified @ishor13’s approach by adding redirects recursively to child routes.

The only downside is that when you navigate to deeply nested routes like this /foo/fooChild -> /bar/barChild, the router still fails, but I solved this with a custom version of routerLink which in this case would first navigate to /bar and then to /bar/barChild, which works fine.

interface IRouteConfigData {
  reuse: boolean;
}

interface ICachedRoute {
  handle: DetachedRouteHandle;
  data: IRouteConfigData;
}

@Injectable()
export class AppReuseStrategy implements RouteReuseStrategy {
  private routeCache = new Map<string, ICachedRoute>();

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    const data = this.getRouteData(route);
    return data && data.reuse;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    const url = this.getFullRouteUrl(route);
    const data = this.getRouteData(route);
    this.routeCache.set(url, { handle, data });
    this.addRedirectsRecursively(route);
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const url = this.getFullRouteUrl(route);
    return this.routeCache.has(url);
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    const url = this.getFullRouteUrl(route);
    const data = this.getRouteData(route);
    return data && data.reuse && this.routeCache.has(url)
      ? this.routeCache.get(url).handle
      : null;
  }

  private addRedirectsRecursively(route: ActivatedRouteSnapshot): void {
    const config = route.routeConfig;
    if (config) {
      if (!config.loadChildren) {
        const routeFirstChild = route.firstChild;
        const routeFirstChildUrl = routeFirstChild
          ? this.getRouteUrlPaths(routeFirstChild).join('/')
          : '';
        const childConfigs = config.children;
        if (childConfigs) {
          const childConfigWithRedirect = childConfigs.find(c => c.path === '' && !!c.redirectTo);
          if (childConfigWithRedirect) {
            childConfigWithRedirect.redirectTo = routeFirstChildUrl;
          }
        }
      }
      route.children.forEach(childRoute => this.addRedirectsRecursively(childRoute));
    }
  }

  private getFullRouteUrl(route: ActivatedRouteSnapshot): string {
    return this.getFullRouteUrlPaths(route).filter(Boolean).join('/');
  }

  private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
    const paths = this.getRouteUrlPaths(route);
    return route.parent
      ? [ ...this.getFullRouteUrlPaths(route.parent), ...paths ]
      : paths;
  }

  private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
    return route.url.map(urlSegment => urlSegment.path);
  }

  private getRouteData(route: ActivatedRouteSnapshot): IRouteConfigData {
    return route.routeConfig && route.routeConfig.data as IRouteConfigData;
  }
}

I had similar problems until I realized that the main issue was what keys I used to store detached route handles. It’s very often that we navigate to children relative to the parent in angular apps. As a result what we get in the methods of RouteReuseStrategy as ActivatedRouteSnapshot is not a final route but some parent one. Using route.routeConfig.path as a key is certainly not enough - it’ll be overriding values every time in the storage which causes Angular to curse Cannot reattach. When I changed key generation to as follows the problem disappeared:

private takeFullUrl(route: ActivatedRouteSnapshot) {
    let next = route;
    // Since navigation is usually relative
    // we go down to find out the child to be shown.
    while (next.firstChild) {
      next = next.firstChild;
    }
    const segments = [];
    // Then build a unique key-path by going to the root.
    while (next) {
      segments.push(next.url.join('/'));
      next = next.parent;
    }
    return compact(segments.reverse()).join('/');
  }

Hope will be useful for someone.

More information from my investigations…

It seems that the reuse strategy in the sample app sticky routes sample works in cases where your app has child routes. It does not seem to work when your app has sibling / auxiliary routes.

In this case the error that you run into is:

Error: Cannot reattach ActivatedRouteSnapshot with a different number of children
  at setFutureSnapshotsOfActivatedRoutes

@flymithra maybe you can ask @wardbell as he closed this issue with the following reason:

The problem is obscure and not of general interest.

10 months later and still no solution. Unreal.

don’t use route pattern use route pattern resolved with params.

I’m going to make an analagy here.

Thank you for ordering your new vehicle with both anti-lock brakes and four wheel drive options. Unfortunately because you have selected both features, you’ll have to disable ABS while your vehicle is in four wheel drive mode because they are incompatible.

I’d argue that it is better to stop pretending to offer routeReuse if it isn’t compatible with lazy loading modules.

2 years passed from I hit this error and still there is no solution for this problem. not good

the following is work! reference:https://www.cnblogs.com/lovesangel/p/7853364.html

import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from ‘@angular/router’;

/**

  • 主要思想:
  • angular 路由就是棵树, 当 url 从 a/b/c/d 切换到 a/e/f/g 时.
  • shouldReuseRoute 会被触发. angular 会让你去对比看是否要 reuse
  • a vs a
  • b vs e
  • c vs f
  • d vs g
  • 一般情况下 a vs a 自然是 reuse
  • b vs e 就替换, 而一旦 parent 被替换, 那么所有 child 自然也是被替换的.
  • NOTE!:Detach和Attach指的是是否从DOM树中脱离和加入DOM树。shouldDetach和shouldAttach的含义为onDeatch和onAttach,意味着在Detach和Attach时用户可以做出某种决定!
  • 替换一旦发生, 就会有某些组件要被丢弃 destroy, 这时 shouldDetech, store 就会被调用, 用于缓存这些已经渲染完成即将被丢弃的组件.
  • 有组件被丢弃自然有组件需要进来替补, 而这时 shouldAttach,retrieve 就会被调用, 用来调出缓存的组件.
  • 所以流程是这样 :
    1. 是否替换 ?
    1. 替换发生, 有组件离去, 有组件加入
    1. 离去的组件, 我们可以缓存
    1. 加入的组件, 我们可以使用缓存好的组件.
  • 替换->缓存->重用 就是整体的核心了. */

export class CustomReuseStrategy implements RouteReuseStrategy {

public static handlers: { [key: string]: DetachedRouteHandle } = {}

private static waitDelete: string

public static deleteRouteSnapshot(name: string): void {
    if (CustomReuseStrategy.handlers[name]) {
        delete CustomReuseStrategy.handlers[name];
    } else {
        CustomReuseStrategy.waitDelete = name;
    }
}
/** 表示对所有路由允许复用 如果你有路由不想利用可以在这加一些业务逻辑判断 */
public shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return true;
}

/** 当路由离开时会触发。按path作为key存储路由快照&组件当前实例对象 */
public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    if (CustomReuseStrategy.waitDelete && CustomReuseStrategy.waitDelete == this.getRouteUrl(route)) {
        // 如果待删除是当前路由则不存储快照
        CustomReuseStrategy.waitDelete = null
        return;
    }
    CustomReuseStrategy.handlers[this.getRouteUrl(route)] = handle
}

/** 若 path 在缓存中有的都认为允许还原路由 */
public shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!CustomReuseStrategy.handlers[this.getRouteUrl(route)]
}

/** 从缓存中获取快照,若无则返回nul */
public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    if (!route.routeConfig) {
        return null
    }

    return CustomReuseStrategy.handlers[this.getRouteUrl(route)]
}

/** 进入路由触发,判断是否同一路由 */
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig &&
        JSON.stringify(future.params) === JSON.stringify(curr.params);
}

private getRouteUrl(route: ActivatedRouteSnapshot) {
    return route['_routerState'].url.replace(/\//g, '_')
}

}

For all who looking for temporary workaround - you can disable Route Reuse just by addnig this in constructor:

import {Router} from "@angular/router";

this.router.routeReuseStrategy.shouldReuseRoute = function(){ return false; };

I concur with @gnovotny. @dmitrimaltsev this is amazing and with some minor tweaks to accommodate the translation of spaces in the path values with the percent 20 replacement I was able to use your solution. Additional kudos to @pcurrivan for his stackblitz project as from there I was able to finish off my solution with the notations for reuse on the route paths.

@dmitrimaltsev this is amazing, thanks

I had the same idea as @barbatus did. Here’s a smaller version doing the same thing.

private getPath(route: ActivatedRouteSnapshot) {
    return route.pathFromRoot
      .map(route => route.routeConfig && route.routeConfig.path)
      .filter(path => !!path)
      .join('/');
  }

Still doesn’t work for routes with children, so I had to flatten my route config a bit.

Following on from @wardbell comment, I guess a further question now comes up:

Is RouteReuseStrategy going to be moved forward ie moved to stable, and common use cases for it’s use in an app that uses child and auxiliary routes documented?

Saw this commit in a sticky routes sample by @manfredsteyer where it looks like he was trying to solve the same problem - maybe this could be used for inspiration?

Further debugging confirmed my finding. It was indeed the outlet mapped passed in getting mixed up

image

On the call of activateRoutes from ActivateRoutes class, the parentOutletMap passed in contains the wrong primary outlet in its _outlets property.

Same issue here. Will there be a solution anytime soon? Look like somewhere in the middle, the children router outlets got swapped. The first child router outlet is used for url meant for the 2nd outlet.