diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index e81c194831..ae073bc39d 100644 --- a/packages/react-router/src/ReactRouter/IonRouter.tsx +++ b/packages/react-router/src/ReactRouter/IonRouter.tsx @@ -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; }; diff --git a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx index 8f34354ae2..9b686cfcba 100644 --- a/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx +++ b/packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx @@ -102,8 +102,6 @@ const resolveIndexRouteMatch = ( }; export class ReactRouterViewStack extends ViewStacks { - private pendingViewItems: Map = new Map(); - private deactivationQueue: Map = 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 | 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); }; } diff --git a/packages/react-router/src/ReactRouter/StackManager.tsx b/packages/react-router/src/ReactRouter/StackManager.tsx index d685004efe..41803125b1 100644 --- a/packages/react-router/src/ReactRouter/StackManager.tsx +++ b/packages/react-router/src/ReactRouter/StackManager.tsx @@ -113,7 +113,6 @@ export class StackManager extends React.PureComponent 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);