Skip to content

Commit f0f496e

Browse files
committed
fix(react-router): scope push cleanup to explicitly preserved views
1 parent f5e6026 commit f0f496e

1 file changed

Lines changed: 33 additions & 8 deletions

File tree

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)