chore(react-router): code clean up

This commit is contained in:
ShaneK
2025-12-02 06:22:57 -08:00
parent 5a77916fd5
commit 397b6f7148
3 changed files with 7 additions and 118 deletions

View File

@@ -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;
};

View File

@@ -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);
};
}

View File

@@ -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);