Skip to content

Commit 643584b

Browse files
authored
fix(vue-router): render the redirect target when a guard redirects (#31510)
Issue number: internal --------- ## What is the current behavior? Currently, when a guard returns a location instead of `false` during a back navigation, the URL updates to the redirect target but Ionic renders a different page. A redirect doesn't fail, so the original navigation never reaches `afterEach` or `onError`. Its staged delta and params are never cleared and get applied to the redirect target instead. ## What is the new behavior? The ownership `beforeEach` now discards the state a navigation claimed when a guard redirects it. We match it through `redirectedFrom`, which vue-router points at the first location in the redirect chain. That covers leave guards, which run before our hook, and a guard redirect to the current page, which fails as a duplicate before reaching `beforeEach`. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information This was [raised while reviewing #31364](#31364 (comment)) and left out of scope there.
1 parent 1c89743 commit 643584b

2 files changed

Lines changed: 370 additions & 11 deletions

File tree

‎packages/vue-router/src/router.ts‎

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,6 @@ export const createIonRouter = (
117117
* instead, so there is no failure to inspect there and the staged state
118118
* would survive. The error is re-thrown to the app the same way it was
119119
* before, so navigation outcomes are unchanged.
120-
*
121-
* A guard that returns a location is still not covered, because that
122-
* redirects rather than fails and afterEach is never called for the original
123-
* navigation.
124120
*/
125121
addErrorHandler((error: unknown, to: RouteLocationNormalized) => {
126122
discardStagedStateFor(to);
@@ -183,20 +179,71 @@ export const createIonRouter = (
183179
incomingRouteParamsUnclaimed = false;
184180
};
185181

182+
/**
183+
* Whether `to` is a guard redirect of the navigation that claimed `owner`.
184+
* vue-router points `redirectedFrom` at the first location in a redirect
185+
* chain, so an owner that a `redirect:` record led to is matched on its own
186+
* `redirectedFrom`.
187+
*/
188+
const isGuardRedirectOf = (
189+
owner: RouteLocationNormalized | undefined,
190+
to: RouteLocationNormalized
191+
) =>
192+
owner !== undefined &&
193+
to.redirectedFrom !== undefined &&
194+
(owner.redirectedFrom ?? owner) === to.redirectedFrom;
195+
196+
/**
197+
* Whether `to` was redirected by a guard rather than a `redirect:` record.
198+
* This can't tell a record redirect apart from one a leave guard redirected
199+
* again afterwards.
200+
*/
201+
const isRedirectedByGuard = (to: RouteLocationNormalized) => {
202+
const origin = to.redirectedFrom;
203+
204+
return (
205+
origin !== undefined &&
206+
origin.matched[origin.matched.length - 1]?.redirect === undefined
207+
);
208+
};
209+
186210
/**
187211
* The navigation that starts first after params are staged is the one they
188212
* were staged for, so it takes ownership of them here. Registered before any
189213
* guard the app adds so that it still runs when one of those aborts.
214+
*
215+
* A guard that returns a location starts a new navigation and the original
216+
* never reaches afterEach or onError, so we discard what it claimed here
217+
* instead. Leave guards run before this hook, so a navigation they redirect
218+
* hasn't claimed anything yet and its state is discarded unclaimed.
190219
*/
191220
router.beforeEach((to: RouteLocationNormalized) => {
221+
if (isGuardRedirectOf(currentNavigationInfoOwner, to)) {
222+
clearNavigationInfo();
223+
}
224+
225+
if (isGuardRedirectOf(incomingRouteParamsOwner, to)) {
226+
clearStagedParams();
227+
}
228+
229+
const redirectedByGuard = isRedirectedByGuard(to);
230+
192231
if (incomingRouteParamsUnclaimed) {
193-
incomingRouteParamsOwner = to;
194-
incomingRouteParamsUnclaimed = false;
232+
if (redirectedByGuard) {
233+
clearStagedParams();
234+
} else {
235+
incomingRouteParamsOwner = to;
236+
incomingRouteParamsUnclaimed = false;
237+
}
195238
}
196239

197240
if (currentNavigationInfoUnclaimed) {
198-
currentNavigationInfoOwner = to;
199-
currentNavigationInfoUnclaimed = false;
241+
if (redirectedByGuard) {
242+
clearNavigationInfo();
243+
} else {
244+
currentNavigationInfoOwner = to;
245+
currentNavigationInfoUnclaimed = false;
246+
}
200247
}
201248
});
202249

@@ -219,18 +266,22 @@ export const createIonRouter = (
219266
* Both are matched on the navigation that owns them rather than on where it
220267
* was heading, because two navigations can head for the same path and a path
221268
* cannot tell them apart. State still unclaimed belongs to this navigation,
222-
* since nothing has started since it was staged.
269+
* since nothing has started since it was staged. A guard redirect that fails
270+
* before reaching beforeEach, like one to the current page, is matched
271+
* through the navigation it redirected.
223272
*/
224273
const discardStagedStateFor = (to: RouteLocationNormalized) => {
225274
const deltaIsForThisNavigation =
226275
currentNavigationInfoOwner === undefined
227276
? currentNavigationInfoUnclaimed
228-
: currentNavigationInfoOwner === to;
277+
: currentNavigationInfoOwner === to ||
278+
isGuardRedirectOf(currentNavigationInfoOwner, to);
229279

230280
const paramsAreForThisNavigation =
231281
incomingRouteParamsOwner === undefined
232282
? incomingRouteParamsUnclaimed
233-
: incomingRouteParamsOwner === to;
283+
: incomingRouteParamsOwner === to ||
284+
isGuardRedirectOf(incomingRouteParamsOwner, to);
234285

235286
if (deltaIsForThisNavigation) {
236287
clearNavigationInfo();

0 commit comments

Comments
 (0)