@@ -113,6 +113,15 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
113113 * duplicate transitions during rapid navigation (e.g., Navigate redirects)
114114 */
115115 private lastTransition ?: { enteringId : string ; leavingId ?: string } ;
116+ /**
117+ * Views that have been explicitly kept alive by the pop-preserve logic
118+ * (shouldPreserveLeavingView) so a future forward-pop can restore their React
119+ * state. These are candidates for cleanup when a fresh push invalidates the
120+ * forward-history path that made them reachable. Views mounted through
121+ * normal forward-push (which keeps the leaving view alive by default) are
122+ * NOT tracked here.
123+ */
124+ private preservedViewItems = new Set < ViewItem > ( ) ;
116125 /** Tracks whether the component is mounted to guard async transition paths. */
117126 private _isMounted = false ;
118127 /** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
@@ -505,6 +514,9 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
505514 if ( ! enteringViewItem . mount ) {
506515 enteringViewItem . mount = true ;
507516 }
517+ // A view that becomes the entering view is no longer a stale preserved view.
518+ // It's back in the active navigation path, so drop it from the cleanup set.
519+ this . preservedViewItems . delete ( enteringViewItem ) ;
508520
509521 // Check visibility state BEFORE showing entering view
510522 const enteringWasVisible = enteringViewItem . ionPageElement && isViewVisible ( enteringViewItem . ionPageElement ) ;
@@ -581,6 +593,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
581593 routeInfo . routeAction === 'pop' && isViewItemPreservableOnPop ( leavingViewItem ) ;
582594 if ( routeInfo . routeAction !== 'replace' && ! shouldPreserveLeavingView ) {
583595 leavingViewItem . mount = false ;
596+ } else if ( shouldPreserveLeavingView ) {
597+ this . preservedViewItems . add ( leavingViewItem ) ;
584598 }
585599 this . handleLeavingViewUnmount ( routeInfo , enteringViewItem , leavingViewItem ) ;
586600 }
@@ -595,9 +609,11 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
595609 }
596610
597611 /**
598- * Unmounts any previously-preserved leaf views in this outlet when a push
599- * action invalidates the forward history path. Runs only on `push` (not
600- * replace/pop) and skips the entering and leaving view items.
612+ * Unmounts views previously kept alive by the pop-preserve logic when a fresh
613+ * push invalidates the forward-history path that made them reachable. Only
614+ * iterates views explicitly tracked in `preservedViewItems` so that views
615+ * naturally mounted through forward-push (the default leaving-view behavior)
616+ * are left untouched.
601617 */
602618 private cleanupPreservedViewsOnPush (
603619 routeInfo : RouteInfo ,
@@ -607,19 +623,21 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
607623 if ( routeInfo . routeAction !== 'push' ) {
608624 return ;
609625 }
626+ if ( this . preservedViewItems . size === 0 ) {
627+ return ;
628+ }
610629
611- const allViews = this . context . getViewItemsForOutlet ( this . id ) ;
612- for ( const viewItem of allViews ) {
630+ for ( const viewItem of Array . from ( this . preservedViewItems ) ) {
613631 if ( viewItem === enteringViewItem || viewItem === leavingViewItem ) {
632+ this . preservedViewItems . delete ( viewItem ) ;
614633 continue ;
615634 }
616635 if ( ! viewItem . mount ) {
617- continue ;
618- }
619- if ( ! isViewItemPreservableOnPop ( viewItem ) ) {
636+ this . preservedViewItems . delete ( viewItem ) ;
620637 continue ;
621638 }
622639 viewItem . mount = false ;
640+ this . preservedViewItems . delete ( viewItem ) ;
623641 const viewToUnmount = viewItem ;
624642 setTimeout ( ( ) => {
625643 this . context . unMountViewItem ( viewToUnmount ) ;
@@ -863,6 +881,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
863881 routeInfo . routeAction === 'pop' && isViewItemPreservableOnPop ( leavingViewItem ) ;
864882 if ( routeInfo . routeAction !== 'replace' && ! shouldPreserveLeavingView ) {
865883 leavingViewItem . mount = false ;
884+ } else if ( shouldPreserveLeavingView ) {
885+ this . preservedViewItems . add ( leavingViewItem ) ;
866886 }
867887 this . handleLeavingViewUnmount ( routeInfo , enteringViewItem , leavingViewItem ) ;
868888 }
@@ -904,6 +924,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
904924 routeInfo . routeAction === 'pop' && isViewItemPreservableOnPop ( latestLeavingView ) ;
905925 if ( routeInfo . routeAction !== 'replace' && ! shouldPreserveLeavingView ) {
906926 latestLeavingView . mount = false ;
927+ } else if ( shouldPreserveLeavingView ) {
928+ this . preservedViewItems . add ( latestLeavingView ) ;
907929 }
908930 this . handleLeavingViewUnmount ( routeInfo , latestEnteringView , latestLeavingView ) ;
909931 }
@@ -1010,6 +1032,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
10101032 this . outOfScopeUnmountTimeout = undefined ;
10111033 }
10121034 this . waitingForIonPage = false ;
1035+ this . preservedViewItems . clear ( ) ;
10131036
10141037 // Hide all views in this outlet before clearing.
10151038 // This is critical for nested outlets - when the parent component unmounts,
@@ -1137,6 +1160,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
11371160 routeInfo . routeAction === 'pop' && isViewItemPreservableOnPop ( leavingViewItem ) ;
11381161 if ( shouldUnmountLeavingViewItem && ! shouldPreserveLeavingView ) {
11391162 leavingViewItem . mount = false ;
1163+ } else if ( shouldUnmountLeavingViewItem && shouldPreserveLeavingView ) {
1164+ this . preservedViewItems . add ( leavingViewItem ) ;
11401165 }
11411166 }
11421167 }
0 commit comments