fix(react-router): cleanup orphaned sibling views after replace navigation

This commit is contained in:
ShaneK
2025-12-17 12:01:32 -08:00
parent 289f6edc67
commit ab3dde2812
2 changed files with 93 additions and 4 deletions

View File

@@ -470,6 +470,11 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
leavingViewItem.mount = false;
this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
}
// Clean up any orphaned sibling views that are no longer reachable
// This is important for replace actions (like redirects) where sibling views
// that were pushed earlier become unreachable
this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
}
/**
@@ -520,6 +525,90 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
}, VIEW_UNMOUNT_DELAY_MS);
}
/**
* Cleans up orphaned sibling views after a replace action.
* When navigating via replace (e.g., through a redirect), sibling views that were
* pushed earlier may become orphaned (unreachable via back navigation).
* This method identifies and unmounts such views.
*/
private cleanupOrphanedSiblingViews(
routeInfo: RouteInfo,
enteringViewItem: ViewItem,
leavingViewItem: ViewItem | undefined
): void {
// Only cleanup for replace actions
if (routeInfo.routeAction !== 'replace') {
return;
}
const enteringRoutePath = enteringViewItem.reactElement?.props?.path as string | undefined;
if (!enteringRoutePath) {
return;
}
// Get all views in this outlet
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
// Check if routes are "siblings" - direct children of the same outlet at the same level
const areSiblingRoutes = (path1: string, path2: string): boolean => {
// Both are relative routes (don't start with /)
const path1IsRelative = !path1.startsWith('/');
const path2IsRelative = !path2.startsWith('/');
// For relative routes at the outlet root level, they're siblings
if (path1IsRelative && path2IsRelative) {
// Check if they're at the same depth (no nested slashes, except for wildcards)
const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
return path1Depth === path2Depth && path1Depth <= 1;
}
// For absolute routes, check if they share the same parent
const getParent = (path: string) => {
const normalized = path.replace(/\/\*$/, '');
const lastSlash = normalized.lastIndexOf('/');
return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
};
return getParent(path1) === getParent(path2);
};
for (const viewItem of allViewsInOutlet) {
const viewRoutePath = viewItem.reactElement?.props?.path as string | undefined;
// Skip views that shouldn't be cleaned up:
// - The entering view itself
// - The immediate leaving view (handled separately by handleLeavingViewUnmount)
// - Already unmounted views
// - Views without a route path
// - Container routes (ending in /*) when entering is also a container route
const shouldSkip =
viewItem.id === enteringViewItem.id ||
(leavingViewItem && viewItem.id === leavingViewItem.id) ||
!viewItem.mount ||
!viewRoutePath ||
(viewRoutePath.endsWith('/*') && enteringRoutePath.endsWith('/*'));
if (shouldSkip) {
continue;
}
// Check if this is a sibling route that should be cleaned up
if (areSiblingRoutes(enteringRoutePath, viewRoutePath)) {
// Hide and unmount the orphaned view
hideIonPageElement(viewItem.ionPageElement);
viewItem.mount = false;
// Schedule removal
const viewToRemove = viewItem;
setTimeout(() => {
this.context.unMountViewItem(viewToRemove);
this.forceUpdate();
}, VIEW_UNMOUNT_DELAY_MS);
}
}
}
/**
* Handles the case when entering view has no ion-page element yet (waiting for render).
*/

View File

@@ -268,7 +268,7 @@ describe('Routing Tests', () => {
cy.ionPageVisible('home-details-page-2');
});
it('/routing/tabs/home Menu > Favorites > Menu > Home with redirect, Home page should be visible, and Favorites should be hidden', () => {
it('/routing/tabs/home Menu > Favorites > Menu > Home with redirect, Home page should be visible, and Favorites should be destroyed', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home`);
cy.ionMenuClick();
cy.ionMenuNav('Favorites');
@@ -276,10 +276,10 @@ describe('Routing Tests', () => {
cy.ionMenuClick();
cy.ionMenuNav('Home with redirect');
cy.ionPageVisible('home-page');
cy.ionPageHidden('favorites-page');
cy.ionPageDoesNotExist('favorites-page');
});
it('/routing/tabs/home Menu > Favorites > Menu > Home with router, Home page should be visible, and Favorites should be hidden', () => {
it('/routing/tabs/home Menu > Favorites > Menu > Home with router, Home page should be visible, and Favorites should be destroyed', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home`);
cy.ionMenuClick();
cy.ionMenuNav('Favorites');
@@ -287,7 +287,7 @@ describe('Routing Tests', () => {
cy.ionMenuClick();
cy.ionMenuNav('Home with router');
cy.ionPageVisible('home-page');
cy.ionPageHidden('favorites-page');
cy.ionPageDoesNotExist('favorites-page');
});
it('should show back button when going back to a pushed page', () => {