fix(router): Only retrieve stored route when reuse strategy indicates it should reattach (#30263)

When creating the router state, the `RouteReuseStrategy#retrieve` should
only be called when `RouteReuseStrategy#shouldAttach` returns `true`.
That is, we should only retrieve a stored route when the reuse strategy
indicates that there is one stored and that it should be reattached.

This now matches the behavior in the route activation:
1d12c50f63/packages/router/src/operators/activate_routes.ts (L170-L172)

Fixes #23162

PR Close #30263
This commit is contained in:
Dmitrij Kuba 2019-05-04 21:18:14 +03:00 committed by Andrew Kushnir
parent 9ffd9f6f37
commit 6bceb709df
3 changed files with 22 additions and 24 deletions

View File

@ -28,22 +28,21 @@ function createNode(
value._futureSnapshot = curr.value; value._futureSnapshot = curr.value;
const children = createOrReuseChildren(routeReuseStrategy, curr, prevState); const children = createOrReuseChildren(routeReuseStrategy, curr, prevState);
return new TreeNode<ActivatedRoute>(value, children); return new TreeNode<ActivatedRoute>(value, children);
// retrieve an activated route that is used to be displayed, but is not currently displayed
} else { } else {
const detachedRouteHandle = if (routeReuseStrategy.shouldAttach(curr.value)) {
<DetachedRouteHandleInternal>routeReuseStrategy.retrieve(curr.value); // retrieve an activated route that is used to be displayed, but is not currently displayed
if (detachedRouteHandle) { const detachedRouteHandle = routeReuseStrategy.retrieve(curr.value);
const tree: TreeNode<ActivatedRoute> = detachedRouteHandle.route; if (detachedRouteHandle !== null) {
const tree = (detachedRouteHandle as DetachedRouteHandleInternal).route;
setFutureSnapshotsOfActivatedRoutes(curr, tree); setFutureSnapshotsOfActivatedRoutes(curr, tree);
return tree; return tree;
}
}
} else {
const value = createActivatedRoute(curr.value); const value = createActivatedRoute(curr.value);
const children = curr.children.map(c => createNode(routeReuseStrategy, c)); const children = curr.children.map(c => createNode(routeReuseStrategy, c));
return new TreeNode<ActivatedRoute>(value, children); return new TreeNode<ActivatedRoute>(value, children);
} }
}
} }
function setFutureSnapshotsOfActivatedRoutes( function setFutureSnapshotsOfActivatedRoutes(

View File

@ -92,25 +92,18 @@ describe('create router state', () => {
checkActivatedRoute(currC[1], ComponentB, 'right'); checkActivatedRoute(currC[1], ComponentB, 'right');
}); });
it('should cache the retrieved routeReuseStrategy', () => { it('should not retrieve routes when `shouldAttach` is always false', () => {
const config = [ const config = [
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'left'} {path: 'c', component: ComponentC, outlet: 'left'}
]; ];
spyOn(reuseStrategy, 'retrieve').and.callThrough(); spyOn(reuseStrategy, 'retrieve');
const prevState = const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState()); createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
advanceState(prevState); advanceState(prevState);
createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
// Expect 2 calls as the baseline setup expect(reuseStrategy.retrieve).not.toHaveBeenCalled();
expect(reuseStrategy.retrieve).toHaveBeenCalledTimes(2);
// This call should produce a reused activated route
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
// Verify the retrieve method has been called one more time
expect(reuseStrategy.retrieve).toHaveBeenCalledTimes(3);
}); });
it('should consistently represent future and current state', () => { it('should consistently represent future and current state', () => {

View File

@ -5807,6 +5807,7 @@ describe('Integration', () => {
const fixture = createRoot(router, RootCmp); const fixture = createRoot(router, RootCmp);
router.routeReuseStrategy = new AttachDetachReuseStrategy(); router.routeReuseStrategy = new AttachDetachReuseStrategy();
spyOn(router.routeReuseStrategy, 'retrieve').and.callThrough();
router.resetConfig([ router.resetConfig([
{ {
@ -5824,14 +5825,19 @@ describe('Integration', () => {
expect(location.path()).toEqual('/a/b'); expect(location.path()).toEqual('/a/b');
expect(teamCmp).toBeDefined(); expect(teamCmp).toBeDefined();
expect(simpleCmp).toBeDefined(); expect(simpleCmp).toBeDefined();
expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled();
router.navigateByUrl('/c'); router.navigateByUrl('/c');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/c'); expect(location.path()).toEqual('/c');
expect(fixture.debugElement.children[1].componentInstance).toBeAnInstanceOf(UserCmp); expect(fixture.debugElement.children[1].componentInstance).toBeAnInstanceOf(UserCmp);
// We have still not encountered a route that should be reattached
expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled();
router.navigateByUrl('/a;p=1/b;p=2'); router.navigateByUrl('/a;p=1/b;p=2');
advance(fixture); advance(fixture);
// We retrieve both the stored route snapshots
expect(router.routeReuseStrategy.retrieve).toHaveBeenCalledTimes(2);
const teamCmp2 = fixture.debugElement.children[1].componentInstance; const teamCmp2 = fixture.debugElement.children[1].componentInstance;
const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance; const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance;
expect(location.path()).toEqual('/a;p=1/b;p=2'); expect(location.path()).toEqual('/a;p=1/b;p=2');