Skip to content

Commit 3fe7571

Browse files
atscottalxhub
authored andcommitted
fix(router): page refresh should not destroy history state (#48540)
The router's `initialNavigation` causes an imperative navigation using the `navigateByUrl` method. This, however, results in the history state being removed on a page refresh. This change calls `scheduleNavigation` directly from `initialNavigation` to ensure the history state is correctly retained. PR Close #48540
1 parent b09a716 commit 3fe7571

File tree

3 files changed

+52
-41
lines changed

3 files changed

+52
-41
lines changed

packages/router/src/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
1919
* @publicApi
2020
*/
2121
export type NavigationTrigger = 'imperative'|'popstate'|'hashchange';
22+
export const IMPERATIVE_NAVIGATION = 'imperative';
2223

2324
/**
2425
* Identifies the type of a router event.

packages/router/src/navigation_transition.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject} from 'rx
1111
import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, tap} from 'rxjs/operators';
1212

1313
import {createRouterState} from './create_router_state';
14-
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
14+
import {Event, GuardsCheckEnd, GuardsCheckStart, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
1515
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
1616
import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error';
1717
import {activateRoutes} from './operators/activate_routes';
@@ -335,7 +335,7 @@ export class NavigationTransitions {
335335
resolve: null,
336336
reject: null,
337337
promise: Promise.resolve(true),
338-
source: 'imperative',
338+
source: IMPERATIVE_NAVIGATION,
339339
restoredState: null,
340340
currentSnapshot: router.routerState.snapshot,
341341
targetSnapshot: null,
@@ -726,11 +726,12 @@ export class NavigationTransitions {
726726
isBrowserTriggeredNavigation(overallTransitionState.source)
727727
};
728728

729-
router.scheduleNavigation(mergedTree, 'imperative', null, extras, {
730-
resolve: overallTransitionState.resolve,
731-
reject: overallTransitionState.reject,
732-
promise: overallTransitionState.promise
733-
});
729+
router.scheduleNavigation(
730+
mergedTree, IMPERATIVE_NAVIGATION, null, extras, {
731+
resolve: overallTransitionState.resolve,
732+
reject: overallTransitionState.reject,
733+
promise: overallTransitionState.promise
734+
});
734735
}
735736

736737
/* All other errors should reset to the router's internal URL reference
@@ -764,6 +765,6 @@ export class NavigationTransitions {
764765
}
765766
}
766767

767-
export function isBrowserTriggeredNavigation(source: 'imperative'|'popstate'|'hashchange') {
768-
return source !== 'imperative';
768+
export function isBrowserTriggeredNavigation(source: NavigationTrigger) {
769+
return source !== IMPERATIVE_NAVIGATION;
769770
}

packages/router/src/router.ts

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {BehaviorSubject, Observable, of, SubscriptionLike} from 'rxjs';
1212

1313
import {CreateUrlTreeStrategy} from './create_url_tree_strategy';
1414
import {RuntimeErrorCode} from './errors';
15-
import {Event, NavigationTrigger} from './events';
15+
import {Event, IMPERATIVE_NAVIGATION, NavigationTrigger} from './events';
1616
import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models';
1717
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
1818
import {TitleStrategy} from './page_title_strategy';
@@ -353,7 +353,8 @@ export class Router {
353353
initialNavigation(): void {
354354
this.setUpLocationChangeListener();
355355
if (!this.navigationTransitions.hasRequestedNavigation) {
356-
this.navigateByUrl(this.location.path(true), {replaceUrl: true});
356+
const state = this.location.getState() as RestoredState;
357+
this.navigateToSyncWithBrowser(this.location.path(true), IMPERATIVE_NAVIGATION, state);
357358
}
358359
}
359360

@@ -373,37 +374,49 @@ export class Router {
373374
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
374375
// hybrid apps.
375376
setTimeout(() => {
376-
const extras: NavigationExtras = {replaceUrl: true};
377-
378-
// TODO: restoredState should always include the entire state, regardless
379-
// of navigationId. This requires a breaking change to update the type on
380-
// NavigationStart’s restoredState, which currently requires navigationId
381-
// to always be present. The Router used to only restore history state if
382-
// a navigationId was present.
383-
384-
// The stored navigationId is used by the RouterScroller to retrieve the scroll
385-
// position for the page.
386-
const restoredState = event.state?.navigationId ? event.state : null;
387-
388-
// Separate to NavigationStart.restoredState, we must also restore the state to
389-
// history.state and generate a new navigationId, since it will be overwritten
390-
if (event.state) {
391-
const stateCopy = {...event.state} as Partial<RestoredState>;
392-
delete stateCopy.navigationId;
393-
delete stateCopy.ɵrouterPageId;
394-
if (Object.keys(stateCopy).length !== 0) {
395-
extras.state = stateCopy;
396-
}
397-
}
398-
399-
const urlTree = this.parseUrl(event['url']!);
400-
this.scheduleNavigation(urlTree, source, restoredState, extras);
377+
this.navigateToSyncWithBrowser(event['url']!, source, event.state);
401378
}, 0);
402379
}
403380
});
404381
}
405382
}
406383

384+
/**
385+
* Schedules a router navigation to synchronize Router state with the browser state.
386+
*
387+
* This is done as a response to a popstate event and the initial navigation. These
388+
* two scenarios represent times when the browser URL/state has been updated and
389+
* the Router needs to respond to ensure its internal state matches.
390+
*/
391+
private navigateToSyncWithBrowser(
392+
url: string, source: NavigationTrigger, state: RestoredState|undefined) {
393+
const extras: NavigationExtras = {replaceUrl: true};
394+
395+
// TODO: restoredState should always include the entire state, regardless
396+
// of navigationId. This requires a breaking change to update the type on
397+
// NavigationStart’s restoredState, which currently requires navigationId
398+
// to always be present. The Router used to only restore history state if
399+
// a navigationId was present.
400+
401+
// The stored navigationId is used by the RouterScroller to retrieve the scroll
402+
// position for the page.
403+
const restoredState = state?.navigationId ? state : null;
404+
405+
// Separate to NavigationStart.restoredState, we must also restore the state to
406+
// history.state and generate a new navigationId, since it will be overwritten
407+
if (state) {
408+
const stateCopy = {...state} as Partial<RestoredState>;
409+
delete stateCopy.navigationId;
410+
delete stateCopy.ɵrouterPageId;
411+
if (Object.keys(stateCopy).length !== 0) {
412+
extras.state = stateCopy;
413+
}
414+
}
415+
416+
const urlTree = this.parseUrl(url);
417+
this.scheduleNavigation(urlTree, source, restoredState, extras);
418+
}
419+
407420
/** The current URL. */
408421
get url(): string {
409422
return this.serializeUrl(this.currentUrlTree);
@@ -561,7 +574,7 @@ export class Router {
561574
const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
562575
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
563576

564-
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
577+
return this.scheduleNavigation(mergedTree, IMPERATIVE_NAVIGATION, null, extras);
565578
}
566579

567580
/**
@@ -686,10 +699,6 @@ export class Router {
686699

687700
let targetPageId: number;
688701
if (this.canceledNavigationResolution === 'computed') {
689-
const isInitialPage = this.currentPageId === 0;
690-
if (isInitialPage) {
691-
restoredState = this.location.getState() as RestoredState | null;
692-
}
693702
// If the `ɵrouterPageId` exist in the state then `targetpageId` should have the value of
694703
// `ɵrouterPageId`. This is the case for something like a page refresh where we assign the
695704
// target id to the previously set value for that page.

0 commit comments

Comments
 (0)