-
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
f1f6a33
ec47c2f
1b77808
c85f647
51e1b8c
99ba2e4
7561004
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,11 @@ export const createIonRouter = ( | |
| _: RouteLocationNormalized, | ||
| failure?: NavigationFailure | void | ||
| ) => { | ||
| if (failure) return; | ||
| if (failure) { | ||
| discardStagedStateFor(to); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const { direction, action, delta } = currentNavigationInfo; | ||
|
|
||
|
|
@@ -68,10 +73,26 @@ export const createIonRouter = ( | |
| direction: undefined, | ||
| action: undefined, | ||
| delta: undefined, | ||
| to: undefined, | ||
| }; | ||
| } | ||
| ); | ||
|
|
||
| /** | ||
| * A guard that throws, including an await on a session check that rejects, | ||
| * never reaches afterEach. vue-router rejects the navigation promise | ||
| * instead, so there is no failure to inspect there and the staged state | ||
| * would survive. This does not handle the error, so navigation outcomes are | ||
| * unchanged. | ||
| * | ||
| * A guard that returns a location is still not covered, because that | ||
| * redirects rather than fails and afterEach is never called for the original | ||
| * navigation. | ||
| */ | ||
| router.onError((_error: unknown, to: RouteLocationNormalized) => { | ||
| discardStagedStateFor(to); | ||
| }); | ||
|
|
||
| const locationHistory = createLocationHistory(); | ||
|
|
||
| /** | ||
|
|
@@ -89,6 +110,98 @@ export const createIonRouter = ( | |
| * Cleared once `handleHistoryChange` has consumed them. | ||
| */ | ||
| let incomingRouteParams: RouteParams | undefined; | ||
| /** | ||
| * The navigation the staged params belong to. vue-router hands the same | ||
| * location object to every hook for one navigation and builds a fresh one | ||
| * per navigation, so it identifies a navigation even when two of them target | ||
| * the same path. Kept beside the params rather than on them so it is never | ||
| * spread onto a RouteInfo and stored in `locationHistory`. | ||
| */ | ||
| let incomingRouteParamsOwner: RouteLocationNormalized | undefined; | ||
| /** | ||
| * Set between staging the params and the navigation claiming them below. | ||
| * A navigation rejected as a duplicate never reaches `beforeEach`, so it | ||
| * never claims, and this is what tells the gate those params are still its | ||
| * own rather than a later navigation's. | ||
| */ | ||
| let incomingRouteParamsUnclaimed = false; | ||
|
|
||
| /** | ||
| * The only place that stages route params, so an owner can never be left | ||
| * over from an earlier navigation. | ||
| */ | ||
| const stageRouteParams = (params: RouteParams) => { | ||
| incomingRouteParams = params; | ||
| incomingRouteParamsOwner = undefined; | ||
| incomingRouteParamsUnclaimed = true; | ||
| }; | ||
|
|
||
| /** | ||
| * The only place that clears them, so an owner can never outlive the params | ||
| * it belongs to and go on to match an unrelated navigation. | ||
| */ | ||
| const clearStagedParams = () => { | ||
| incomingRouteParams = undefined; | ||
| incomingRouteParamsOwner = undefined; | ||
| incomingRouteParamsUnclaimed = false; | ||
| }; | ||
|
|
||
| /** | ||
| * The navigation that starts first after params are staged is the one they | ||
| * were staged for, so it takes ownership of them here. Registered before any | ||
| * guard the app adds so that it still runs when one of those aborts. | ||
| */ | ||
| router.beforeEach((to: RouteLocationNormalized) => { | ||
| if (incomingRouteParamsUnclaimed) { | ||
| incomingRouteParamsOwner = to; | ||
| incomingRouteParamsUnclaimed = false; | ||
| } | ||
| }); | ||
|
|
||
| /** | ||
| * State staged for a navigation that did not complete describes something | ||
| * that did not happen. handleHistoryChange normally consumes it, but it does | ||
| * not run for a navigation that failed, so it has to be discarded 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. A stale set of params carries an action, a direction and | ||
| * sometimes a tab or a previous route's id into whatever runs next. | ||
| * | ||
| * Only discard state belonging to this navigation, and check the two slots | ||
| * separately. Another navigation can replace this one and stage its own | ||
| * state first, in which case discarding would strip that state from the | ||
| * navigation still running. | ||
| * | ||
| * The params are matched on the navigation that owns them rather than on | ||
| * where it was heading, because two navigations can head for the same path | ||
| * and a path cannot tell them apart. Params still unclaimed belong to this | ||
| * navigation, since nothing has started since they were staged. | ||
| */ | ||
| const discardStagedStateFor = (to: RouteLocationNormalized) => { | ||
| const deltaIsForThisNavigation = | ||
| currentNavigationInfo.to === undefined || | ||
| currentNavigationInfo.to === to.fullPath; | ||
|
|
||
| const paramsAreForThisNavigation = | ||
| incomingRouteParamsOwner === undefined | ||
| ? incomingRouteParamsUnclaimed | ||
| : incomingRouteParamsOwner === to; | ||
|
|
||
| if (deltaIsForThisNavigation) { | ||
| currentNavigationInfo = { | ||
| direction: undefined, | ||
| action: undefined, | ||
| delta: undefined, | ||
| to: undefined, | ||
| }; | ||
| } | ||
|
|
||
| if (paramsAreForThisNavigation) { | ||
| clearStagedParams(); | ||
| } | ||
| }; | ||
|
|
||
| const historyChangeListeners: any[] = []; | ||
|
|
||
|
|
@@ -101,7 +214,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 +236,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 +261,12 @@ export const createIonRouter = ( | |
| if (routeInfo && routeInfo.pushedByRoute) { | ||
| const prevInfo = locationHistory.findLastLocation(routeInfo); | ||
| if (prevInfo) { | ||
| incomingRouteParams = { | ||
| stageRouteParams({ | ||
|
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. I suggested leaving this target undefined to fall back to the delta, but I missed the non-linear
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. Changed the approach rather than stamping this branch: params are now matched on the navigation that owns them instead of where it was heading, so this branch stages unclaimed and its Couldn't get a spec onto this branch though. Nothing in the suite reaches it, and every sequence I tried came out with a negative |
||
| ...prevInfo, | ||
| routerAction: "pop", | ||
| routerDirection: "back", | ||
| routerAnimation: routerAnimation || routeInfo.routerAnimation, | ||
| }; | ||
| }); | ||
| if ( | ||
| routeInfo.lastPathname === routeInfo.pushedByRoute || | ||
| /** | ||
|
|
@@ -222,7 +341,7 @@ export const createIonRouter = ( | |
| * There is nowhere to navigate, so drop the params rather than | ||
| * letting them leak into the next navigation. | ||
| */ | ||
| incomingRouteParams = undefined; | ||
| clearStagedParams(); | ||
| } | ||
| } | ||
| } else if (defaultHref) { | ||
|
|
@@ -585,7 +704,7 @@ export const createIonRouter = ( | |
|
|
||
| currentRouteInfo = routeInfo; | ||
| } | ||
| incomingRouteParams = undefined; | ||
| clearStagedParams(); | ||
| historyChangeListeners.forEach((cb) => cb(currentRouteInfo)); | ||
| }; | ||
|
|
||
|
|
@@ -665,13 +784,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 +796,20 @@ 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, | ||
| }); | ||
|
|
||
| router.push(target); | ||
| } else { | ||
| handleNavigate( | ||
| pathname + hrefSearch + hrefHash, | ||
|
|
@@ -792,12 +909,12 @@ export const createIonRouter = ( | |
| routerAnimation?: AnimationBuilder, | ||
| tab?: string | ||
| ) => { | ||
| incomingRouteParams = { | ||
| stageRouteParams({ | ||
| routerAction, | ||
| routerDirection, | ||
| routerAnimation, | ||
| tab, | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| const goBack = (routerAnimation?: AnimationBuilder) => { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.