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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy