mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react-router): cleanup orphaned sibling views after replace navigation
This commit is contained in:
@@ -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).
|
||||
*/
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user