mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore(react-router): code clean up
This commit is contained in:
@@ -153,19 +153,16 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
|
||||
}
|
||||
|
||||
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
||||
// Check if the URL has changed.
|
||||
if (leavingUrl !== location.pathname) {
|
||||
// An external navigation was triggered.
|
||||
if (!incomingRouteParams.current) {
|
||||
// Determine if the destination is a tab route by checking if it matches
|
||||
// the pattern of tab routes (containing /tabs/ in the path)
|
||||
const isTabRoute = /\/tabs(\/|$)/.test(location.pathname);
|
||||
let tabToUse = isTabRoute ? currentTab.current : undefined;
|
||||
const tabToUse = isTabRoute ? currentTab.current : undefined;
|
||||
|
||||
// If we're leaving tabs entirely, clear the current tab
|
||||
if (!isTabRoute && currentTab.current) {
|
||||
currentTab.current = undefined;
|
||||
tabToUse = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,7 +198,6 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
|
||||
};
|
||||
}
|
||||
}
|
||||
// Still found no params, set it to a default state of forward.
|
||||
if (!incomingRouteParams.current) {
|
||||
const state = location.state as LocationState | null;
|
||||
incomingRouteParams.current = {
|
||||
@@ -247,9 +243,8 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
|
||||
params: incomingRouteParams.current?.params
|
||||
? filterUndefinedParams(incomingRouteParams.current.params as RouteParams)
|
||||
: {},
|
||||
prevRouteLastPathname: leavingLocationInfo.lastPathname, // The lastPathname of the route we are leaving
|
||||
prevRouteLastPathname: leavingLocationInfo.lastPathname,
|
||||
};
|
||||
// It's a linear navigation.
|
||||
if (isPushed) {
|
||||
// Only inherit tab from leaving route if we don't already have one.
|
||||
// This preserves tab context for same-tab navigation while allowing cross-tab navigation.
|
||||
@@ -312,7 +307,6 @@ export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildr
|
||||
setRouteInfo(routeInfo);
|
||||
}
|
||||
|
||||
// Reset for the next navigation.
|
||||
incomingRouteParams.current = null;
|
||||
};
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ const resolveIndexRouteMatch = (
|
||||
};
|
||||
|
||||
export class ReactRouterViewStack extends ViewStacks {
|
||||
private pendingViewItems: Map<string, ViewItem> = new Map();
|
||||
private deactivationQueue: Map<string, NodeJS.Timeout> = new Map();
|
||||
private viewItemCounter = 0;
|
||||
|
||||
constructor() {
|
||||
@@ -182,21 +180,9 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
return existingViewItem;
|
||||
}
|
||||
|
||||
// Create a truly unique ID by combining outlet ID with an incrementing counter
|
||||
this.viewItemCounter++;
|
||||
const id = `${outletId}-${this.viewItemCounter}`;
|
||||
|
||||
// Add infinite loop detection with a more reasonable limit
|
||||
// In complex navigation flows, we may have many view items across different outlets
|
||||
if (this.viewItemCounter > 100) {
|
||||
// Clean up all outlets to prevent memory leaks
|
||||
this.getStackIds().forEach((stackId) => this.cleanupStaleViewItems(stackId));
|
||||
// Reset counter to a lower value after cleanup
|
||||
if (this.viewItemCounter > 100) {
|
||||
this.viewItemCounter = 50;
|
||||
}
|
||||
}
|
||||
|
||||
const viewItem: ViewItem = {
|
||||
id,
|
||||
outletId,
|
||||
@@ -219,9 +205,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
childProps: reactElement.props,
|
||||
};
|
||||
|
||||
// Store in pending until properly added
|
||||
const key = `${outletId}-${routeInfo.pathname}`;
|
||||
this.pendingViewItems.set(key, viewItem);
|
||||
this.add(viewItem);
|
||||
|
||||
return viewItem;
|
||||
};
|
||||
@@ -254,14 +238,6 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
// Flag to indicate this view should not be reused for this different parameterized path
|
||||
const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
|
||||
|
||||
// Cancel any pending deactivation if we have a match
|
||||
if (match) {
|
||||
const timeoutId = this.deactivationQueue.get(viewItem.id);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.deactivationQueue.delete(viewItem.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't deactivate views automatically - let the StackManager handle view lifecycle
|
||||
// This preserves views in the stack for navigation history like native apps
|
||||
@@ -342,9 +318,7 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
|
||||
// Check if this view item would match the current route
|
||||
const vMatch = v.reactElement ? matchComponent(v.reactElement, routeInfo.pathname) : null;
|
||||
const hasMatch = !!vMatch;
|
||||
|
||||
return hasMatch;
|
||||
return !!vMatch;
|
||||
});
|
||||
|
||||
if (hasSpecificMatch) {
|
||||
@@ -655,7 +629,6 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Render all view items using renderViewItem
|
||||
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
|
||||
return renderedItems;
|
||||
};
|
||||
@@ -702,25 +675,6 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
let match: PathMatch<string> | null = null;
|
||||
let viewStack: ViewItem[];
|
||||
|
||||
// First check pending items
|
||||
if (outletId) {
|
||||
const pendingKey = `${outletId}-${pathname}`;
|
||||
const pendingItem = this.pendingViewItems.get(pendingKey);
|
||||
if (pendingItem) {
|
||||
// Move from pending to active
|
||||
this.pendingViewItems.delete(pendingKey);
|
||||
this.add(pendingItem);
|
||||
return { viewItem: pendingItem, match: pendingItem.routeData.match };
|
||||
}
|
||||
|
||||
// Fallback: If we did not find a pending item for this outlet, look for
|
||||
// a pending view item created under a different outlet for the same pathname
|
||||
// and adopt it into the current outlet. This can happen if an outlet remounts
|
||||
// (e.g., during browser forward navigation) and gets a new generated id.
|
||||
// Disable cross-outlet adoption for now; it can cause mismatches where
|
||||
// views are moved between outlets with different routing scopes.
|
||||
}
|
||||
|
||||
// Helper function to sort views by specificity (most specific first)
|
||||
const sortBySpecificity = (views: ViewItem[]) => {
|
||||
return [...views].sort((a, b) => {
|
||||
@@ -885,19 +839,13 @@ export class ReactRouterViewStack extends ViewStacks {
|
||||
|
||||
super.add(viewItem);
|
||||
|
||||
// Clean up stale view items after adding new ones
|
||||
this.cleanupStaleViewItems(viewItem.outletId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Override remove to clear any pending deactivations
|
||||
* Override remove
|
||||
*/
|
||||
remove = (viewItem: ViewItem) => {
|
||||
const timeoutId = this.deactivationQueue.get(viewItem.id);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.deactivationQueue.delete(viewItem.id);
|
||||
}
|
||||
super.remove(viewItem);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
this.registerIonPage = this.registerIonPage.bind(this);
|
||||
this.transitionPage = this.transitionPage.bind(this);
|
||||
this.handlePageTransition = this.handlePageTransition.bind(this);
|
||||
// Use provided id prop if available; otherwise generate a unique id.
|
||||
this.id = props.id || `routerOutlet-${generateId('routerOutlet')}`;
|
||||
this.prevProps = undefined;
|
||||
this.skipTransition = false;
|
||||
@@ -436,13 +435,6 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
return false;
|
||||
}
|
||||
|
||||
// routeOptions.unmount should NOT trigger view removal - it just hides the view
|
||||
// The view needs to remain in the stack for tab navigation and back navigation
|
||||
// Don't set mount = false, just let the transition hide the page
|
||||
// if (routeInfo.routeOptions?.unmount) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
if (routeInfo.routeAction === 'replace') {
|
||||
return true;
|
||||
}
|
||||
@@ -799,16 +791,12 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
||||
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
if (shouldUnmountLeavingViewItem && leavingViewItem) {
|
||||
if (shouldUnmountLeavingViewItem) {
|
||||
leavingViewItem.mount = false;
|
||||
}
|
||||
} else {
|
||||
// No entering or leaving view - this might be a routing issue
|
||||
// Don't retry endlessly to avoid infinite loops
|
||||
}
|
||||
}
|
||||
|
||||
// Force re-render so views update according to their new mount/visible status
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
@@ -1054,11 +1042,8 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
|
||||
await runCommit(enteringViewItem.ionPageElement, undefined);
|
||||
}
|
||||
} else {
|
||||
// The leaving view is not the same as the entering view
|
||||
// (e.g., `/home` → `/settings` or initial load `/`)
|
||||
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
|
||||
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
|
||||
// An initiial load will not have a leaving view.
|
||||
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
||||
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
@@ -1213,7 +1198,7 @@ function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, paren
|
||||
if (absolutePathRoutes.length > 0) {
|
||||
// Find common prefix of all absolute paths to determine outlet scope
|
||||
const absolutePaths = absolutePathRoutes.map((r) => r.props.path as string);
|
||||
const commonPrefix = findCommonPrefix(absolutePaths);
|
||||
const commonPrefix = computeCommonPrefix(absolutePaths);
|
||||
|
||||
// If we have a common prefix, check if the current pathname is within that scope
|
||||
if (commonPrefix && commonPrefix !== '/') {
|
||||
@@ -1234,44 +1219,6 @@ function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, paren
|
||||
return matchedNode ?? fallbackNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the longest common prefix among an array of paths.
|
||||
* Used to determine the scope of an outlet with absolute routes.
|
||||
*/
|
||||
function findCommonPrefix(paths: string[]): string {
|
||||
if (paths.length === 0) return '';
|
||||
if (paths.length === 1) {
|
||||
// For a single path, extract the directory-like prefix
|
||||
// e.g., /dynamic-routes/home -> /dynamic-routes
|
||||
const segments = paths[0].split('/').filter(Boolean);
|
||||
if (segments.length > 1) {
|
||||
return '/' + segments.slice(0, -1).join('/');
|
||||
}
|
||||
return '/' + segments[0];
|
||||
}
|
||||
|
||||
// Split all paths into segments
|
||||
const segmentArrays = paths.map((p) => p.split('/').filter(Boolean));
|
||||
const minLength = Math.min(...segmentArrays.map((s) => s.length));
|
||||
|
||||
const commonSegments: string[] = [];
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
const segment = segmentArrays[0][i];
|
||||
// Skip segments with route parameters or wildcards
|
||||
if (segment.includes(':') || segment.includes('*')) {
|
||||
break;
|
||||
}
|
||||
const allMatch = segmentArrays.every((s) => s[i] === segment);
|
||||
if (allMatch) {
|
||||
commonSegments.push(segment);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
|
||||
}
|
||||
|
||||
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
|
||||
const routePath: string | undefined = node?.props?.path;
|
||||
const pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
||||
|
||||
Reference in New Issue
Block a user