mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react-router): use reRender callback in getChildrenToRender to clean up stale views
This commit is contained in:
@@ -339,7 +339,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
* - Wraps the route element in <Routes> to support nested routing and ensure remounting
|
||||
* - Adds a unique key to <Routes> so React Router remounts routes when switching
|
||||
*/
|
||||
private renderViewItem = (viewItem: ViewItem, routeInfo: RouteInfo, parentPath?: string) => {
|
||||
private renderViewItem = (viewItem: ViewItem, routeInfo: RouteInfo, parentPath?: string, reRender?: () => void) => {
|
||||
const routePath = viewItem.reactElement.props.path || '';
|
||||
let match = matchComponent(viewItem.reactElement, routeInfo.pathname);
|
||||
|
||||
@@ -383,6 +383,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
// This ensures the redirect completes before removal
|
||||
setTimeout(() => {
|
||||
this.remove(viewItem);
|
||||
reRender?.();
|
||||
}, NAVIGATE_REDIRECT_DELAY_MS);
|
||||
}
|
||||
}
|
||||
@@ -409,6 +410,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
|
||||
if (stillNotNeeded) {
|
||||
this.remove(viewItem);
|
||||
reRender?.();
|
||||
}
|
||||
}, VIEW_CLEANUP_DELAY_MS);
|
||||
} else {
|
||||
@@ -522,7 +524,12 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
* 3. Returns a list of React components that will be rendered inside the outlet
|
||||
* Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
|
||||
*/
|
||||
getChildrenToRender = (outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) => {
|
||||
getChildrenToRender = (
|
||||
outletId: string,
|
||||
ionRouterOutlet: React.ReactElement,
|
||||
routeInfo: RouteInfo,
|
||||
reRender: () => void
|
||||
) => {
|
||||
const viewItems = this.getViewItemsForOutlet(outletId);
|
||||
|
||||
// Determine parentPath for nested outlets to properly evaluate index routes
|
||||
@@ -621,6 +628,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
// View is outside current route hierarchy, remove it
|
||||
setTimeout(() => {
|
||||
this.remove(viewItem);
|
||||
reRender();
|
||||
}, 0);
|
||||
return false;
|
||||
}
|
||||
@@ -630,7 +638,9 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
return true;
|
||||
});
|
||||
|
||||
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
|
||||
const renderedItems = renderableViewItems.map((viewItem) =>
|
||||
this.renderViewItem(viewItem, routeInfo, parentPath, reRender)
|
||||
);
|
||||
return renderedItems;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import ContentChangeNavigation from './pages/content-change-navigation/ContentCh
|
||||
import SearchParams from './pages/search-params/SearchParams';
|
||||
import IonRoutePropsTest from './pages/ion-route-props/IonRouteProps';
|
||||
import PrefixMatchWildcard from './pages/prefix-match-wildcard/PrefixMatchWildcard';
|
||||
import StaleViewCleanup from './pages/stale-view-cleanup/StaleViewCleanup';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
@@ -87,6 +88,7 @@ const App: React.FC = () => {
|
||||
<Route path="/search-params" element={<SearchParams />} />
|
||||
<Route path="/ion-route-props/*" element={<IonRoutePropsTest />} />
|
||||
<Route path="/prefix-match-wildcard/*" element={<PrefixMatchWildcard />} />
|
||||
<Route path="/stale-view-cleanup/*" element={<StaleViewCleanup />} />
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
|
||||
@@ -92,6 +92,9 @@ const Main: React.FC = () => {
|
||||
<IonItem routerLink="/prefix-match-wildcard">
|
||||
<IonLabel>Prefix Match Wildcard</IonLabel>
|
||||
</IonItem>
|
||||
<IonItem routerLink="/stale-view-cleanup/non-ionpage">
|
||||
<IonLabel>Stale View Cleanup</IonLabel>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IonRouterOutlet } from '@ionic/react';
|
||||
import React from 'react';
|
||||
import { Route, useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* A component without IonPage wrapper.
|
||||
* When navigated away from, this should be cleaned up from the DOM.
|
||||
*/
|
||||
const NonIonPageSource: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div data-testid="non-ionpage-source">
|
||||
<h1>Non-IonPage Source</h1>
|
||||
<button id="go-to-target" onClick={() => navigate('/stale-view-cleanup/target')}>
|
||||
Go to Target
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NonIonPageTarget: React.FC = () => {
|
||||
return (
|
||||
<div data-testid="non-ionpage-target">
|
||||
<div data-testid="target-loaded">Target page loaded</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StaleViewCleanup: React.FC = () => {
|
||||
return (
|
||||
<IonRouterOutlet>
|
||||
<Route path="non-ionpage" element={<NonIonPageSource />} />
|
||||
<Route path="target" element={<NonIonPageTarget />} />
|
||||
</IonRouterOutlet>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaleViewCleanup;
|
||||
@@ -0,0 +1,24 @@
|
||||
const port = 3000;
|
||||
|
||||
describe('Stale View Cleanup', () => {
|
||||
/**
|
||||
* Tests that non-IonPage components are properly cleaned up from the DOM
|
||||
* after navigating away. Both source and target lack IonPage wrappers.
|
||||
* Guards against regressions in the view stack cleanup logic.
|
||||
*/
|
||||
it('should clean up stale non-IonPage view after navigating to another non-IonPage view', () => {
|
||||
cy.visit(`http://localhost:${port}/stale-view-cleanup/non-ionpage`);
|
||||
|
||||
// Verify the non-IonPage source component is visible
|
||||
cy.get('[data-testid="non-ionpage-source"]').should('exist');
|
||||
|
||||
// Navigate to the target (also non-IonPage)
|
||||
cy.get('#go-to-target').click();
|
||||
|
||||
// Verify the target loaded
|
||||
cy.get('[data-testid="target-loaded"]').should('exist');
|
||||
|
||||
// The non-IonPage source component should be cleaned up from the DOM
|
||||
cy.get('[data-testid="non-ionpage-source"]').should('not.exist');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user