-
Notifications
You must be signed in to change notification settings - Fork 13.3k
fix(vue-router): clear navigation info when a guard aborts navigation #31364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e023609
6d5c199
7ae3b16
b6c6e73
d4b89b7
4bbcc7a
2dc6efd
7ff2c5e
b1b2aca
1b50d95
101143e
64a77a6
398cb36
06e0174
67125fe
6856c6f
6a6032f
f40d2d1
f469679
150ecb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ export const createIonRouter = ( | |
| direction: undefined, | ||
| action: undefined, | ||
| delta: undefined, | ||
| to: undefined, | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -46,7 +47,59 @@ export const createIonRouter = ( | |
| _: RouteLocationNormalized, | ||
| failure?: NavigationFailure | void | ||
| ) => { | ||
| if (failure) return; | ||
| if (failure) { | ||
| /** | ||
| * State staged for a navigation that failed describes something that | ||
| * did not happen. handleHistoryChange normally consumes it, but it does | ||
| * not run when the navigation fails, so it has to be cleared here or | ||
| * the next navigation picks it up instead. | ||
| * | ||
| * A delta is only staged for a history navigation, and a stale one | ||
| * makes the next navigation look like traversal, which stops the | ||
| * incoming route from being added. Route params are staged by any of | ||
| * the navigation helpers, and a stale set carries a pop action into | ||
| * whatever runs next. Only handleNavigateBack stages the previous | ||
| * route's id alongside them. | ||
| * | ||
| * Only clear state that belongs to this navigation, and check the two | ||
| * slots separately. Another navigation can replace this one and stage | ||
| * its own state first, in which case clearing would strip that state | ||
| * from the navigation still running. | ||
| * | ||
| * Params staged without a target fall back to the delta's target. | ||
| * Those come from the helpers that hand off to history, so a delta is | ||
| * always recorded for them. | ||
| * | ||
| * This only covers navigations that fail. A guard that returns a | ||
| * location redirects rather than fails, so afterEach is never called | ||
| * for the original navigation and its staged state reaches the redirect | ||
| * target instead. | ||
| */ | ||
| const deltaIsForThisNavigation = | ||
| currentNavigationInfo.to === undefined || | ||
| currentNavigationInfo.to === to.fullPath; | ||
|
|
||
| const paramsAreForThisNavigation = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like only half of this made it in. Stamping happens in I hit this with a global async guard, the session-check kind. Tap a link, then tap the Tab 2 button before the first one finishes, and you get It's wider than I expected, too. On a 400ms guard I tried gaps from 10ms all the way up to about 400ms and every one of them did it, main was right on all of them. Makes sense in hindsight, the first navigation starts its guard first so it always reports the cancellation before the second one finishes. It does need a guard though. With plain lazy routes the tab's chunk is already cached by the time The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| incomingRouteParamsTo === undefined | ||
| ? deltaIsForThisNavigation | ||
| : incomingRouteParamsTo === to.fullPath; | ||
|
|
||
| if (deltaIsForThisNavigation) { | ||
| currentNavigationInfo = { | ||
| direction: undefined, | ||
| action: undefined, | ||
| delta: undefined, | ||
| to: undefined, | ||
| }; | ||
| } | ||
|
|
||
| if (paramsAreForThisNavigation) { | ||
| incomingRouteParams = undefined; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The gate here was my suggestion, and I missed something when I proposed it. Saving the target path works for the delta, but this line clears That regresses against main. With a push in flight on a lazy route, a logout I think stamping the resolved path onto
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went with the stamp. One thing I'd like your read on. I kept the target in a separate
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, keep it as it is. Your reason holds, and I think there's a better version of it. Since The shape isn't really what's biting you though, and I left a comment on the gate about that. Folding |
||
| incomingRouteParamsTo = undefined; | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const { direction, action, delta } = currentNavigationInfo; | ||
|
|
||
|
|
@@ -68,6 +121,7 @@ export const createIonRouter = ( | |
| direction: undefined, | ||
| action: undefined, | ||
| delta: undefined, | ||
| to: undefined, | ||
| }; | ||
| } | ||
| ); | ||
|
|
@@ -89,6 +143,24 @@ export const createIonRouter = ( | |
| * Cleared once `handleHistoryChange` has consumed them. | ||
| */ | ||
| let incomingRouteParams: RouteParams | undefined; | ||
| /** | ||
| * The location the staged params were meant for. Kept beside the params | ||
| * rather than on them so it is never spread onto a RouteInfo. Left undefined | ||
| * by the helpers that hand off to history and so cannot know the target yet, | ||
| * which is `goBack`, `goForward` and `handleNavigateBack`. Those fall back to | ||
| * the delta's target, which history always records for them. | ||
| */ | ||
| let incomingRouteParamsTo: string | undefined; | ||
|
|
||
| /** | ||
| * The only place that stages route params, so the recorded location can | ||
| * never be left over from an earlier navigation. Pass the target when it is | ||
| * known, and omit it to fall back to the delta. | ||
| */ | ||
| const stageRouteParams = (params: RouteParams, to?: RouteLocationRaw) => { | ||
| incomingRouteParams = params; | ||
| incomingRouteParamsTo = to ? router.resolve(to).fullPath : undefined; | ||
| }; | ||
|
|
||
| const historyChangeListeners: any[] = []; | ||
|
|
||
|
|
@@ -101,7 +173,7 @@ export const createIonRouter = ( | |
| }); | ||
| } | ||
|
|
||
| opts.history.listen((_: any, _x: any, info: any) => { | ||
| opts.history.listen((to: any, _x: any, info: any) => { | ||
| /** | ||
| * history.listen only fires on certain | ||
| * event such as when the user clicks the | ||
|
|
@@ -123,6 +195,12 @@ export const createIonRouter = ( | |
| */ | ||
| action: info.type === "pop" && info.delta >= 1 ? "push" : info.type, | ||
| direction: info.direction === "" ? "forward" : info.direction, | ||
|
|
||
| /** | ||
| * Recorded so that a failed navigation can tell whether this | ||
| * information is its own before clearing it. | ||
| */ | ||
| to, | ||
| }; | ||
| }); | ||
|
|
||
|
|
@@ -142,12 +220,12 @@ export const createIonRouter = ( | |
| if (routeInfo && routeInfo.pushedByRoute) { | ||
| const prevInfo = locationHistory.findLastLocation(routeInfo); | ||
| if (prevInfo) { | ||
| incomingRouteParams = { | ||
| stageRouteParams({ | ||
| ...prevInfo, | ||
| routerAction: "pop", | ||
| routerDirection: "back", | ||
| routerAnimation: routerAnimation || routeInfo.routerAnimation, | ||
| }; | ||
| }); | ||
| if ( | ||
| routeInfo.lastPathname === routeInfo.pushedByRoute || | ||
| /** | ||
|
|
@@ -223,6 +301,7 @@ export const createIonRouter = ( | |
| * letting them leak into the next navigation. | ||
| */ | ||
| incomingRouteParams = undefined; | ||
| incomingRouteParamsTo = undefined; | ||
| } | ||
| } | ||
| } else if (defaultHref) { | ||
|
|
@@ -240,7 +319,13 @@ export const createIonRouter = ( | |
| routerAnimation?: AnimationBuilder, | ||
| tab?: string | ||
| ) => { | ||
| setIncomingRouteParams(routerAction, routerDirection, routerAnimation, tab); | ||
| setIncomingRouteParams( | ||
| routerAction, | ||
| routerDirection, | ||
| routerAnimation, | ||
| tab, | ||
| path | ||
| ); | ||
|
|
||
| if (routerAction === "push") { | ||
| router.push(path); | ||
|
|
@@ -586,6 +671,7 @@ export const createIonRouter = ( | |
| currentRouteInfo = routeInfo; | ||
| } | ||
| incomingRouteParams = undefined; | ||
| incomingRouteParamsTo = undefined; | ||
| historyChangeListeners.forEach((cb) => cb(currentRouteInfo)); | ||
| }; | ||
|
|
||
|
|
@@ -601,7 +687,13 @@ export const createIonRouter = ( | |
| const navigate = (navigationOptions: ExternalNavigationOptions) => { | ||
| const { routerAnimation, routerDirection, routerLink } = navigationOptions; | ||
|
|
||
| setIncomingRouteParams("push", routerDirection, routerAnimation); | ||
| setIncomingRouteParams( | ||
| "push", | ||
| routerDirection, | ||
| routerAnimation, | ||
| undefined, | ||
| routerLink | ||
| ); | ||
|
|
||
| router.push(routerLink); | ||
| }; | ||
|
|
@@ -665,13 +757,6 @@ export const createIonRouter = ( | |
| const hrefSearch = search ? `?${search}` : ""; | ||
|
|
||
| if (routeInfo) { | ||
| incomingRouteParams = { | ||
| ...incomingRouteParams, | ||
| routerAction: "push", | ||
| routerDirection: "none", | ||
| tab, | ||
| }; | ||
|
|
||
| /** | ||
| * When going back to a tab | ||
| * you just left, it's possible | ||
|
|
@@ -684,15 +769,23 @@ export const createIonRouter = ( | |
| * are honored when re-selecting the tab. | ||
| */ | ||
| const effectiveSearch = hrefSearch || routeInfo.search || ""; | ||
| const push = { | ||
| const target = { | ||
| path: routeInfo.pathname === pathname ? routeInfo.pathname : pathname, | ||
| query: parseQuery(effectiveSearch), | ||
| ...(hrefHash ? { hash: hrefHash } : {}), | ||
| }; | ||
| if (routeInfo.pathname === pathname) { | ||
| router.push({ path: routeInfo.pathname, ...push }); | ||
| } else { | ||
| router.push({ path: pathname, ...push }); | ||
| } | ||
|
|
||
| stageRouteParams( | ||
| { | ||
| ...incomingRouteParams, | ||
| routerAction: "push", | ||
| routerDirection: "none", | ||
| tab, | ||
| }, | ||
| target | ||
| ); | ||
|
|
||
| router.push(target); | ||
| } else { | ||
| handleNavigate( | ||
| pathname + hrefSearch + hrefHash, | ||
|
|
@@ -790,14 +883,18 @@ export const createIonRouter = ( | |
| routerAction: RouteAction = "push", | ||
| routerDirection: RouteDirection = "forward", | ||
| routerAnimation?: AnimationBuilder, | ||
| tab?: string | ||
| tab?: string, | ||
| to?: RouteLocationRaw | ||
| ) => { | ||
| incomingRouteParams = { | ||
| routerAction, | ||
| routerDirection, | ||
| routerAnimation, | ||
| tab, | ||
| }; | ||
| stageRouteParams( | ||
| { | ||
| routerAction, | ||
| routerDirection, | ||
| routerAnimation, | ||
| tab, | ||
| }, | ||
| to | ||
| ); | ||
| }; | ||
|
|
||
| const goBack = (routerAnimation?: AnimationBuilder) => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An async guard that throws instead of returning
falsenever gets here at all, so the original bug still reproduces. InternallytriggerErrorhands back a rejected promise, so the.thenthat would calltriggerAfterEachnever runs and the.catch(noop)on the end eats it. No failure object, noafterEach.You don't need a deliberate
throweither, anawaiton a session check that rejects does it. On/profile, tap back, guard throws, then tap a link to/settings: the URL says/settingsbut Ionic reports/homewith apop, and the stack collapses down to just Home, so Settings never mounts and Profile gets destroyed. Same on main so nothing regressed, it's just not covered.Different thing from FW-7699, that one's the guard-returning-a-location case. I think a
router.onErrordoing the same two gated clears would close it, andonErrordoesn't swallow the error so it wouldn't change any navigation outcomes. Feels cheap enough to do here, but I'm fine with a card if you'd rather keep this PR tight.