Skip to content

Commit b02c197

Browse files
committed
fix(react-router): prevent incorrect view reuse for parameterized routes
1 parent 584dcf2 commit b02c197

File tree

5 files changed

+201
-166
lines changed

5 files changed

+201
-166
lines changed

packages/react-router/src/ReactRouter/IonRouter.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
253253
// If this is the first time entering this tab from a different context,
254254
// use the leaving route's pathname as the pushedByRoute to maintain the back stack.
255255
routeInfo.pushedByRoute = lastRoute?.pushedByRoute ?? leavingLocationInfo.pathname;
256-
console.log('[IonRouter TAB SWITCH] pathname=' + routeInfo.pathname + ' tab=' + routeInfo.tab + ' leavingTab=' + leavingLocationInfo.tab + ' leavingPathname=' + leavingLocationInfo.pathname + ' lastRoutePushedBy=' + (lastRoute?.pushedByRoute || 'undefined') + ' FINAL_pushedByRoute=' + routeInfo.pushedByRoute);
257256
// Triggered by `history.replace()` or a `<Redirect />` component, etc.
258257
} else if (routeInfo.routeAction === 'replace') {
259258
/**
@@ -408,11 +407,9 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
408407
const config = getConfig();
409408
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
410409
const routeInfo = locationHistory.current.current();
411-
console.log('[IonRouter BACK] START currentPath=' + (routeInfo?.pathname || 'undefined') + ' currentTab=' + (routeInfo?.tab || 'undefined') + ' pushedByRoute=' + (routeInfo?.pushedByRoute || 'undefined'));
412410
// It's a linear navigation.
413411
if (routeInfo && routeInfo.pushedByRoute) {
414412
const prevInfo = locationHistory.current.findLastLocation(routeInfo);
415-
console.log('[IonRouter BACK] findLastLocation result: prevPath=' + (prevInfo?.pathname || 'undefined') + ' prevTab=' + (prevInfo?.tab || 'undefined'));
416413
if (prevInfo) {
417414
/**
418415
* This needs to be passed to handleNavigate
@@ -432,24 +429,20 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
432429
*/
433430
const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
434431
const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '';
435-
console.log('[IonRouter BACK] Decision: condition1=' + condition1 + ' (lastPathname=' + routeInfo.lastPathname + ' == pushedByRoute=' + routeInfo.pushedByRoute + ') condition2=' + condition2 + ' (prevPath=' + prevInfo.pathname + ' == pushedByRoute=' + routeInfo.pushedByRoute + ' && currentTab=' + routeInfo.tab + ' && prevTab=' + prevInfo.tab + ')');
436432
if (condition1 || condition2) {
437-
console.log('[IonRouter BACK] Using navigate(-1) - LINEAR navigation');
438433
navigate(-1);
439434
} else {
440435
/**
441436
* It's a non-linear back navigation.
442437
* e.g., direct link or tab switch or nested navigation with redirects
443438
*/
444-
console.log('[IonRouter BACK] Using handleNavigate - NON-LINEAR navigation to: ' + prevInfo.pathname);
445439
handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation);
446440
}
447441
/**
448442
* `pushedByRoute` exists, but no corresponding previous entry in
449443
* the history stack.
450444
*/
451445
} else {
452-
console.log('[IonRouter BACK] No prevInfo found! Using defaultHref: ' + defaultHref);
453446
handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation);
454447
}
455448
/**

packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ export class ReactRouterViewStack extends ViewStacks {
143143
// Special case: reuse tabs/* and other specific wildcard routes
144144
// Don't reuse index routes (empty path) or generic catch-all wildcards (*)
145145
if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
146+
// For parameterized routes (containing :param), only reuse if the ACTUAL pathname matches
147+
// This ensures /details/1 and /details/2 get separate view items and component instances
148+
const hasParams = routePath.includes(':');
149+
if (hasParams) {
150+
// Check if the existing view item's pathname matches the new pathname
151+
const existingPathname = v.routeData?.match?.pathname;
152+
if (existingPathname !== routeInfo.pathname) {
153+
return false; // Different param values, don't reuse
154+
}
155+
}
146156
return true;
147157
}
148158
// Also reuse specific wildcard routes like tabs/*
@@ -153,9 +163,6 @@ export class ReactRouterViewStack extends ViewStacks {
153163
});
154164

155165
if (existingViewItem) {
156-
console.log(
157-
`[ReactRouterViewStack] Reusing existing view item ${existingViewItem.id} for route ${routeInfo.pathname}`
158-
);
159166
// Update and ensure the existing view item is properly configured
160167
existingViewItem.reactElement = reactElement;
161168
existingViewItem.mount = true;
@@ -177,10 +184,6 @@ export class ReactRouterViewStack extends ViewStacks {
177184
this.viewItemCounter++;
178185
const id = `${outletId}-${this.viewItemCounter}`;
179186

180-
console.log(
181-
`[ReactRouterViewStack] Creating new view item ${id} for route ${routeInfo.pathname} with path: ${routePath}`
182-
);
183-
184187
// Add infinite loop detection with a more reasonable limit
185188
// In complex navigation flows, we may have many view items across different outlets
186189
if (this.viewItemCounter > 100) {
@@ -240,6 +243,15 @@ export class ReactRouterViewStack extends ViewStacks {
240243
}
241244
}
242245

246+
// For parameterized routes, check if this is a navigation to a different path instance
247+
// In that case, we should NOT reuse this view - a new view should be created
248+
const isParameterRoute = routePath.includes(':');
249+
const previousMatch = viewItem.routeData?.match;
250+
const isSamePath = match?.pathname === previousMatch?.pathname;
251+
252+
// Flag to indicate this view should not be reused for this different parameterized path
253+
const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
254+
243255
// Cancel any pending deactivation if we have a match
244256
if (match) {
245257
const timeoutId = this.deactivationQueue.get(viewItem.id);
@@ -261,13 +273,9 @@ export class ReactRouterViewStack extends ViewStacks {
261273
(typeof elementComponent.type === 'function' && elementComponent.type.name === 'Navigate'));
262274

263275
if (isNavigateComponent) {
264-
console.log(
265-
`[ReactRouterViewStack] Navigate component detected for ${viewItem.id}, match=${!!match}, mount=${viewItem.mount}`
266-
);
267276
// Navigate components should only be mounted when they match
268277
// Once they redirect (no longer match), they should be removed completely
269278
if (!match && viewItem.mount) {
270-
console.log(`[ReactRouterViewStack] Unmounting Navigate component ${viewItem.id} after redirect`);
271279
viewItem.mount = false;
272280
// Schedule removal of the Navigate view item after a short delay
273281
// This ensures the redirect completes before removal
@@ -306,7 +314,8 @@ export class ReactRouterViewStack extends ViewStacks {
306314
}
307315

308316
// Reactivate view if it matches but was previously deactivated
309-
if (match && !viewItem.mount) {
317+
// Don't reactivate if this is a parameterized route navigating to a different path instance
318+
if (match && !viewItem.mount && !shouldSkipForDifferentParam) {
310319
viewItem.mount = true;
311320
viewItem.routeData.match = match;
312321
}
@@ -334,10 +343,12 @@ export class ReactRouterViewStack extends ViewStacks {
334343

335344
const routeElement = React.cloneElement(viewItem.reactElement);
336345
const componentElement = routeElement.props.element;
337-
if (match && viewItem.routeData.match !== match) {
346+
// Don't update match for parameterized routes navigating to different path instances
347+
// This preserves the original match so that findViewItemByPath can correctly skip this view
348+
if (match && viewItem.routeData.match !== match && !shouldSkipForDifferentParam) {
338349
viewItem.routeData.match = match;
339350
}
340-
const routeMatch = match || viewItem.routeData?.match;
351+
const routeMatch = shouldSkipForDifferentParam ? viewItem.routeData?.match : (match || viewItem.routeData?.match);
341352

342353
return (
343354
<RouteContext.Consumer key={`view-context-${viewItem.id}`}>
@@ -756,6 +767,13 @@ export class ReactRouterViewStack extends ViewStacks {
756767
return false;
757768
}
758769

770+
// For parameterized routes, never reuse if the pathname is different
771+
// This ensures /details/1 and /details/2 get separate view items
772+
const isParameterRoute = viewItemPath.includes(':');
773+
if (isParameterRoute && !isSamePath) {
774+
return false;
775+
}
776+
759777
// For routes without params, or when navigating to the exact same path,
760778
// or when there's no previous match, reuse the view item
761779
if (!hasParams || isSamePath || !previousMatch) {
@@ -764,12 +782,9 @@ export class ReactRouterViewStack extends ViewStacks {
764782
return true;
765783
}
766784

767-
// For parameterized/wildcard routes, only reuse if the pathname exactly matches
768-
// This prevents reusing /details/1 when navigating to /details/2
785+
// For wildcard routes, only reuse if the pathname exactly matches
769786
const isWildcardRoute = viewItemPath.includes('*');
770-
const isParameterRoute = viewItemPath.includes(':');
771-
772-
if ((isParameterRoute || isWildcardRoute) && isSamePath) {
787+
if (isWildcardRoute && isSamePath) {
773788
match = result;
774789
viewItem = v;
775790
return true;

0 commit comments

Comments
 (0)