3

I implemented RouteReuseStrategy suggested here and also updated a bit, because on shouldAttach the routeConfig.path was empty, and the handler was not cached. I have @angular/router: 3.4.7.

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

export class CustomReuseStrategy implements RouteReuseStrategy {

    handlers: {[key: string]: DetachedRouteHandle} = {};
    currentPath: string = '';

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return true
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        this.handlers[route.routeConfig.path] = handle
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        // todo route.routeConfig.path was empty
        if (!!this.currentPath && !!this.handlers[this.currentPath]) {
            route.routeConfig.path = this.currentPath;
            return true
        } else {
            return false
        }
    }

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

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        this.currentPath = curr.routeConfig && curr.routeConfig.path;

        return future.routeConfig === curr.routeConfig
    }

    deleteHandler(handler:string) {
        this.handlers[handler] && delete this.handlers[handler]
    }

}

Everything is working, except when I try to navigate back the 3rd time to a route I got the following error:

EXCEPTION: Uncaught (in promise): Error: Cannot reattach ActivatedRouteSnapshot with a different number of children
Error: Cannot reattach ActivatedRouteSnapshot with a different number of children
    at setFutureSnapshotsOfActivatedRoutes (http://localhost:4200/js/vendor.bundle.js:93110:15) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93091:9) [angular]
    at http://localhost:4200/js/vendor.bundle.js:93131:16 [angular]
    at Array.map (native) [angular]
    at createOrReuseChildren (http://localhost:4200/js/vendor.bundle.js:93124:26) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93086:41) [angular]
    at http://localhost:4200/js/vendor.bundle.js:93128:24 [angular]
    at Array.map (native) [angular]
    at createOrReuseChildren (http://localhost:4200/js/vendor.bundle.js:93124:26) [angular]
    at createNode (http://localhost:4200/js/vendor.bundle.js:93086:41) [angular]
    at createRouterState (http://localhost:4200/js/vendor.bundle.js:93072:33) [angular]
    at MapSubscriber.project (http://localhost:4200/js/vendor.bundle.js:26366:153) [angular]
    at MapSubscriber._next (http://localhost:4200/js/vendor.bundle.js:15890:35) [angular]
    at MapSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at MergeMapSubscriber.notifyNext (http://localhost:4200/js/vendor.bundle.js:19133:30) [angular]
    at InnerSubscriber._next (http://localhost:4200/js/vendor.bundle.js:106234:21) [angular]
    at InnerSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at MapSubscriber._next (http://localhost:4200/js/vendor.bundle.js:15896:26) [angular]
    at MapSubscriber.Subscriber.next (http://localhost:4200/js/vendor.bundle.js:4861:18) [angular]
    at ReduceSubscriber._complete (http://localhost:4200/js/vendor.bundle.js:36808:30) [angular]
    at ReduceSubscriber.Subscriber.complete (http://localhost:4200/js/vendor.bundle.js:4886:18) [angular]
    at MergeMapSubscriber._complete (http://localhost:4200/js/vendor.bundle.js:19125:30) [angular]
    at MergeMapSubscriber.Subscriber.complete (http://localhost:4200/js/vendor.bundle.js:4886:18) [angular]
    at ArrayObservable._subscribe (http://localhost:4200/js/vendor.bundle.js:12465:24) [angular]
    at ArrayObservable.Observable._trySubscribe (http://localhost:4200/js/vendor.bundle.js:221:25) [angular]
    at ArrayObservable.Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:209:27) [angular]
    at MergeMapOperator.call (http://localhost:4200/js/vendor.bundle.js:19075:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at ReduceOperator.call (http://localhost:4200/js/vendor.bundle.js:36763:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at MapOperator.call (http://localhost:4200/js/vendor.bundle.js:15867:23) [angular]
    at Observable.subscribe (http://localhost:4200/js/vendor.bundle.js:206:22) [angular]
    at Object.subscribeToResult (http://localhost:4200/js/vendor.bundle.js:5096:27) [angular]
...
Community
  • 1
  • 1
István
  • 5,057
  • 10
  • 38
  • 67

1 Answers1

6

I have same problem and was trying for last one week. Seems like it works fine for me now. Main challenge is defining a unique key for any given routes. I find RouteReuseStrategy become bit complex to understand and define key for each route when your configure you app with child routing and lazy loading. I try to explain my understanding and I hope this information helpful.

RouteReuseStrategy
I was trying to figure out the RouteReuseStrategy method calls and parameter details to generate unique key and come up with following diagram by debugging my application. Noticed sometimes stage parameters passed to method calls and try to ignore those parameters in RouteReuseStrategy implementation.

RouteReuseStrategy Method Call Details

CustomReuseStrategy

RouteReuseStrategy Implementation:

/* tslint:disable */
import {ActivatedRouteSnapshot, DetachedRouteHandle, Params, RouteReuseStrategy} from "@angular/router";

interface RouteStorageObject {
    snapshot: ActivatedRouteSnapshot;
    handle: DetachedRouteHandle;
}

export class CustomReuseStrategy implements RouteReuseStrategy {
    storedRoutes: { [key: string]: RouteStorageObject } = {};

    getFurthestDecendantParams(route: ActivatedRouteSnapshot, sum: any): ActivatedRouteSnapshot {
        if (route.children.length > 0) {
            let child: ActivatedRouteSnapshot = route.children[0];
            sum.sum                           = sum.sum + this.sumParams(child.params);
            return this.getFurthestDecendantParams(child, sum);
        }
        return route;
    }

    sumParams(params: Params): string {
        return Object.keys(params).reduce((param, key) => {
            return param + params[key];
        }, "");
    }

    calcKey(route: ActivatedRouteSnapshot) {
        let paramKey = {
            sum: ""
        }
        if (route.children.length > 0) {
            this.getFurthestDecendantParams(route, paramKey).params;
        } else {
            paramKey.sum = this.sumParams(route.params);
        }
        if (paramKey.sum != "") {
            paramKey.sum = "_" + paramKey.sum;
        }
        return route.data.key + paramKey.sum;
    }

    public shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.info("CustomReuseStrategy.shouldDetach() - route key: " + this.calcKey(route));
        return !("product" === this.calcKey(route));
    }


    public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        let storedRoute: RouteStorageObject = {
            snapshot: route,
            handle: handle
        };
        console.info("CustomReuseStrategy.store() - route key: " + this.calcKey(route));
        this.storedRoutes[this.calcKey(route)] = storedRoute;
    }


    public shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.info("CustomReuseStrategy.shouldAttach() - route key: " + this.calcKey(route));
        return this.storedRoutes[this.calcKey(route)] !== undefined;
    }

    public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        console.info("CustomReuseStrategy.retrieve() - route key: " + this.calcKey(route));
        if (this.storedRoutes[this.calcKey(route)] === undefined) {
            /* Just return undefined */
            return null;
        } else {
            return this.storedRoutes[this.calcKey(route)].handle;
        }
    }


    public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        let returnValue = (future.routeConfig === curr.routeConfig);
        if (future.routeConfig != null && curr.routeConfig != null) {
            returnValue = this.calcKey(future) === this.calcKey(curr);
            console.info("CustomReuseStrategy.shouldReuseRoute() - future: " + this.calcKey(future) + ", curr: " + this.calcKey(curr) +
                ", future.routeConfig.path:" + future.routeConfig.path + ", curr.routeConfig.path:" + curr.routeConfig.path + ", returnValue: " + returnValue);
        } else {
            console.info("CustomReuseStrategy.shouldReuseRoute() - future: " + this.calcKey(future) + ", curr: " + this.calcKey(curr) +
                ", future.routeConfig:" + future.routeConfig + ", curr.routeConfig:" + curr.routeConfig + ", returnValue: " + returnValue);
        }
        return returnValue;
    }
}

In Summary

Following things define in my app and RouteReuseStrategy works fine.

  • Defined routes with data key and children lazy loading
  • Only single route-outlet
  • Unique key for each routes
  • Angular Version 2.4.10 and Router Version 3.4.10

Reference

nayakam
  • 4,149
  • 7
  • 42
  • 62
  • nayakam, what is "product" here? – user1892775 Oct 01 '17 at 05:46
  • @user1892775 that is the data key configured for route. – nayakam Oct 01 '17 at 13:06
  • nayakam, I used your approach for my implementation and I am having some issue. Perhaps you can help since you are so familiar with it. Would really appreciate it. This is my post: https://stackoverflow.com/questions/46509980/angular2-customresuestrategy-when-using-combination-of-child-and-sibling-route – user1892775 Oct 01 '17 at 15:42
  • @nayakam Would you please see my issue relates to this? https://stackoverflow.com/questions/56218581/angular-cannot-reattach-activatedroutesnapshot-with-a-different-number-of-childr – Vahid Najafi May 22 '19 at 10:12