@@ -209,34 +209,15 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
209209 }
210210
211211 if ( routeInfo . routeAction === 'replace' ) {
212- const enteringRoutePath = enteringViewItem ?. reactElement ?. props ?. path as string | undefined ;
213212 const leavingRoutePath = leavingViewItem ?. reactElement ?. props ?. path as string | undefined ;
214213
215- // Never unmount root path - needed for back navigation
216- if ( leavingRoutePath === '/' || leavingRoutePath === '' ) {
214+ // Never unmount root path or views without a path - needed for back navigation
215+ if ( ! leavingRoutePath || leavingRoutePath === '/' || leavingRoutePath === '' ) {
217216 return false ;
218217 }
219218
220- if ( enteringRoutePath && leavingRoutePath ) {
221- const getParentPath = ( path : string ) => {
222- const normalized = path . replace ( / \/ \* $ / , '' ) ;
223- const lastSlash = normalized . lastIndexOf ( '/' ) ;
224- return lastSlash > 0 ? normalized . substring ( 0 , lastSlash ) : '/' ;
225- } ;
226-
227- const enteringParent = getParentPath ( enteringRoutePath ) ;
228- const leavingParent = getParentPath ( leavingRoutePath ) ;
229-
230- // Unmount if routes are siblings or entering is a child of leaving (redirect)
231- const areSiblings = enteringParent === leavingParent && enteringParent !== '/' ;
232- const isChildRedirect =
233- enteringRoutePath . startsWith ( leavingRoutePath ) ||
234- ( leavingRoutePath . endsWith ( '/*' ) && enteringRoutePath . startsWith ( leavingRoutePath . slice ( 0 , - 2 ) ) ) ;
235-
236- return areSiblings || isChildRedirect ;
237- }
238-
239- return false ;
219+ // Replace actions unmount the leaving view since it's being replaced in history.
220+ return true ;
240221 }
241222
242223 // For non-replace actions, only unmount for back navigation
@@ -460,7 +441,12 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
460441 this . transitionPage ( routeInfo , enteringViewItem , leavingViewItem , undefined , false , shouldSkipAnimation ) ;
461442
462443 if ( shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem ) {
463- leavingViewItem . mount = false ;
444+ // For non-replace actions (back nav), set mount=false here to hide the view.
445+ // For replace actions, handleLeavingViewUnmount sets mount=false only after
446+ // its container-to-container guard passes, avoiding zombie state.
447+ if ( routeInfo . routeAction !== 'replace' ) {
448+ leavingViewItem . mount = false ;
449+ }
464450 this . handleLeavingViewUnmount ( routeInfo , enteringViewItem , leavingViewItem ) ;
465451 }
466452
@@ -472,12 +458,18 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
472458 * Handles leaving view unmount for replace actions.
473459 */
474460 private handleLeavingViewUnmount ( routeInfo : RouteInfo , enteringViewItem : ViewItem , leavingViewItem : ViewItem ) : void {
475- if ( ! leavingViewItem . ionPageElement ) {
461+ // Only replace actions unmount views; push/pop cache for navigation history
462+ if ( routeInfo . routeAction !== 'replace' ) {
476463 return ;
477464 }
478465
479- // Only replace actions unmount views; push/pop cache for navigation history
480- if ( routeInfo . routeAction !== 'replace' ) {
466+ if ( ! leavingViewItem . ionPageElement ) {
467+ leavingViewItem . mount = false ;
468+ const viewToUnmount = leavingViewItem ;
469+ setTimeout ( ( ) => {
470+ this . context . unMountViewItem ( viewToUnmount ) ;
471+ this . forceUpdate ( ) ;
472+ } , VIEW_UNMOUNT_DELAY_MS ) ;
481473 return ;
482474 }
483475
@@ -497,6 +489,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
497489 return ;
498490 }
499491
492+ leavingViewItem . mount = false ;
493+
500494 const viewToUnmount = leavingViewItem ;
501495 setTimeout ( ( ) => {
502496 this . context . unMountViewItem ( viewToUnmount ) ;
@@ -553,11 +547,25 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
553547
554548 const getParent = ( path : string ) => {
555549 const normalized = path . replace ( / \/ \* $ / , '' ) ;
556- const lastSlash = normalized . lastIndexOf ( '/' ) ;
557- return lastSlash > 0 ? normalized . substring ( 0 , lastSlash ) : '/' ;
550+ const segments = normalized . split ( '/' ) . filter ( Boolean ) ;
551+ // Strip trailing parameter segments (e.g., :id) so that
552+ // sibling routes like /items/list/:id and /items/detail/:id
553+ // resolve to the same parent (/items).
554+ while ( segments . length > 0 && segments [ segments . length - 1 ] . startsWith ( ':' ) ) {
555+ segments . pop ( ) ;
556+ }
557+ segments . pop ( ) ;
558+ return segments . length > 0 ? '/' + segments . join ( '/' ) : '/' ;
558559 } ;
559560
560- return getParent ( path1 ) === getParent ( path2 ) ;
561+ const parent = getParent ( path1 ) ;
562+ // Exclude root-level routes from sibling detection to avoid unintended
563+ // cleanup of unrelated top-level routes. Also covers single-depth param
564+ // routes (e.g., /items/:id) which resolve to root after param stripping.
565+ if ( parent === '/' ) {
566+ return false ;
567+ }
568+ return parent === getParent ( path2 ) ;
561569 } ;
562570
563571 for ( const viewItem of allViewsInOutlet ) {
@@ -568,13 +576,23 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
568576 ( leavingViewItem && viewItem . id === leavingViewItem . id ) ||
569577 ! viewItem . mount ||
570578 ! viewRoutePath ||
579+ // Don't clean up container routes when entering a container route
580+ // (e.g., /tabs/* and /settings/* coexist for tab switching)
571581 ( viewRoutePath . endsWith ( '/*' ) && enteringRoutePath . endsWith ( '/*' ) ) ;
572582
573583 if ( shouldSkip ) {
574584 continue ;
575585 }
576586
577- if ( areSiblingRoutes ( enteringRoutePath , viewRoutePath ) ) {
587+ const isOrphanedSpecificRoute = ! viewRoutePath . endsWith ( '/*' ) ;
588+
589+ // Clean up sibling non-container routes that are no longer reachable.
590+ let shouldCleanup = false ;
591+ if ( ( isReplaceAction || isPushToContainer ) && isOrphanedSpecificRoute ) {
592+ shouldCleanup = areSiblingRoutes ( enteringRoutePath , viewRoutePath ) ;
593+ }
594+
595+ if ( shouldCleanup ) {
578596 hideIonPageElement ( viewItem . ionPageElement ) ;
579597 viewItem . mount = false ;
580598
@@ -667,7 +685,10 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
667685
668686 // Don't unmount if entering and leaving are the same view item
669687 if ( shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem ) {
670- leavingViewItem . mount = false ;
688+ if ( routeInfo . routeAction !== 'replace' ) {
689+ leavingViewItem . mount = false ;
690+ }
691+ this . handleLeavingViewUnmount ( routeInfo , enteringViewItem , leavingViewItem ) ;
671692 }
672693
673694 this . forceUpdate ( ) ;
@@ -701,8 +722,9 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
701722 this . transitionPage ( routeInfo , latestEnteringView , latestLeavingView ?? undefined , undefined , false , shouldSkipAnimation ) ;
702723
703724 if ( shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView ) {
704- latestLeavingView . mount = false ;
705- // Call handleLeavingViewUnmount to ensure the view is properly removed
725+ if ( routeInfo . routeAction !== 'replace' ) {
726+ latestLeavingView . mount = false ;
727+ }
706728 this . handleLeavingViewUnmount ( routeInfo , latestEnteringView , latestLeavingView ) ;
707729 }
708730
0 commit comments