Skip to content
Merged
64 changes: 42 additions & 22 deletions packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,30 +384,38 @@ export class ReactRouterViewStack extends ViewStacks {

// For relative route paths, we need to compute an absolute pathnameBase
// by combining the parent's pathnameBase with the matched portion
let absolutePathnameBase = routeMatch?.pathnameBase || routeInfo.pathname;
const routePath = routeElement.props.path;
const isRelativePath = routePath && !routePath.startsWith('/');
const isIndexRoute = !!routeElement.props.index;

if (isRelativePath || isIndexRoute) {
// Get the parent's pathnameBase to build the absolute path
const parentPathnameBase =
parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';

// For relative paths, the matchPath returns a relative pathnameBase
// We need to make it absolute by prepending the parent's base
if (routeMatch?.pathnameBase && isRelativePath) {
// Strip leading slash if present in the relative match
const relativeBase = routeMatch.pathnameBase.startsWith('/')
? routeMatch.pathnameBase.slice(1)
: routeMatch.pathnameBase;

absolutePathnameBase =
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
} else if (isIndexRoute) {
// Index routes should use the parent's base as their base
absolutePathnameBase = parentPathnameBase;
}
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';

// Get parent's pathnameBase for relative path resolution
const parentPathnameBase =
parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';

// Start with the match's pathnameBase, falling back to routeInfo.pathname
// BUT: splat-only routes should use parent's base (v7_relativeSplatPath behavior)
let absolutePathnameBase: string;

if (isSplatOnlyRoute) {
// Splat routes should NOT contribute their matched portion to pathnameBase
// This aligns with React Router v7's v7_relativeSplatPath behavior
// Without this, relative links inside splat routes get double path segments
absolutePathnameBase = parentPathnameBase;
} else if (isRelativePath && routeMatch?.pathnameBase) {
// For relative paths with a pathnameBase, combine with parent
const relativeBase = routeMatch.pathnameBase.startsWith('/')
? routeMatch.pathnameBase.slice(1)
: routeMatch.pathnameBase;

absolutePathnameBase =
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
} else if (isIndexRoute) {
// Index routes should use the parent's base as their base
absolutePathnameBase = parentPathnameBase;
} else {
// Default: use the match's pathnameBase or the current pathname
absolutePathnameBase = routeMatch?.pathnameBase || routeInfo.pathname;
}

const contextMatches = [
Expand Down Expand Up @@ -469,7 +477,9 @@ export class ReactRouterViewStack extends ViewStacks {
let parentPath: string | undefined = undefined;
try {
// Only attempt parent path computation for non-root outlets
if (outletId !== 'routerOutlet') {
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
const isRootOutlet = outletId.startsWith('routerOutlet');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is being used in several files, why not create a utility for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, I haven't up until this point because it's such a simple single line of code, but I do agree that it probably should still be extracted into a utility - especially because it's not impossible that it changes at some point in the future. I think most of the refactoring I'll save until I have the RR6 branch more consolidated, I still have stuff pretty spread out and I'm not even sure how similar this file is in my next PR. But I think after the main RR6 branch is approved and all of the sub branches are merged into it, a single refactoring sweep would be a good idea to help clean up things like this and extremely large code blocks.

if (!isRootOutlet) {
const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);

Expand Down Expand Up @@ -713,7 +723,17 @@ export class ReactRouterViewStack extends ViewStacks {
return false;
}

// For empty path routes, only match if we're at the same level as when the view was created.
// This prevents an empty path view item from being reused for different routes.
if (isDefaultRoute) {
const previousPathnameBase = v.routeData?.match?.pathnameBase || '';
const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
const normalizedPathname = normalizePathnameForComparison(pathname);

if (normalizedPathname !== normalizedBase) {
return false;
}

match = {
params: {},
pathname,
Expand Down
Loading
Loading