Skip to content

Commit 07104c2

Browse files
authored
fix(react-router): support React Router 6 style relative paths in IonRouterOutlet (#30844)
Issue number: resolves [an issue from a comment](#24177 (comment)) --------- ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Currently, Ionic's RR6 doesn't support relative routes in the same way RR6 does. Routes that do not start with a `/`do not work in the Ionic RR6 implementation in some cases. ## What is the new behavior? With this change, we properly support these route styles and more closely align with normal RR6 route support. ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information Current dev build: ``` 8.7.12-dev.11765307927.1f491e92 ``` This PR will be merged into the RR6 branch, which will be squash+merged into the major 9 branch. This will prevent major 9 from having commits in the change log on release that reference fixing things that are only available in major 9.
1 parent 6a61ecf commit 07104c2

File tree

14 files changed

+1311
-101
lines changed

14 files changed

+1311
-101
lines changed

packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -384,30 +384,38 @@ export class ReactRouterViewStack extends ViewStacks {
384384

385385
// For relative route paths, we need to compute an absolute pathnameBase
386386
// by combining the parent's pathnameBase with the matched portion
387-
let absolutePathnameBase = routeMatch?.pathnameBase || routeInfo.pathname;
388387
const routePath = routeElement.props.path;
389388
const isRelativePath = routePath && !routePath.startsWith('/');
390389
const isIndexRoute = !!routeElement.props.index;
391-
392-
if (isRelativePath || isIndexRoute) {
393-
// Get the parent's pathnameBase to build the absolute path
394-
const parentPathnameBase =
395-
parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
396-
397-
// For relative paths, the matchPath returns a relative pathnameBase
398-
// We need to make it absolute by prepending the parent's base
399-
if (routeMatch?.pathnameBase && isRelativePath) {
400-
// Strip leading slash if present in the relative match
401-
const relativeBase = routeMatch.pathnameBase.startsWith('/')
402-
? routeMatch.pathnameBase.slice(1)
403-
: routeMatch.pathnameBase;
404-
405-
absolutePathnameBase =
406-
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
407-
} else if (isIndexRoute) {
408-
// Index routes should use the parent's base as their base
409-
absolutePathnameBase = parentPathnameBase;
410-
}
390+
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
391+
392+
// Get parent's pathnameBase for relative path resolution
393+
const parentPathnameBase =
394+
parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
395+
396+
// Start with the match's pathnameBase, falling back to routeInfo.pathname
397+
// BUT: splat-only routes should use parent's base (v7_relativeSplatPath behavior)
398+
let absolutePathnameBase: string;
399+
400+
if (isSplatOnlyRoute) {
401+
// Splat routes should NOT contribute their matched portion to pathnameBase
402+
// This aligns with React Router v7's v7_relativeSplatPath behavior
403+
// Without this, relative links inside splat routes get double path segments
404+
absolutePathnameBase = parentPathnameBase;
405+
} else if (isRelativePath && routeMatch?.pathnameBase) {
406+
// For relative paths with a pathnameBase, combine with parent
407+
const relativeBase = routeMatch.pathnameBase.startsWith('/')
408+
? routeMatch.pathnameBase.slice(1)
409+
: routeMatch.pathnameBase;
410+
411+
absolutePathnameBase =
412+
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
413+
} else if (isIndexRoute) {
414+
// Index routes should use the parent's base as their base
415+
absolutePathnameBase = parentPathnameBase;
416+
} else {
417+
// Default: use the match's pathnameBase or the current pathname
418+
absolutePathnameBase = routeMatch?.pathnameBase || routeInfo.pathname;
411419
}
412420

413421
const contextMatches = [
@@ -469,7 +477,9 @@ export class ReactRouterViewStack extends ViewStacks {
469477
let parentPath: string | undefined = undefined;
470478
try {
471479
// Only attempt parent path computation for non-root outlets
472-
if (outletId !== 'routerOutlet') {
480+
// Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
481+
const isRootOutlet = outletId.startsWith('routerOutlet');
482+
if (!isRootOutlet) {
473483
const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
474484
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
475485

@@ -713,7 +723,17 @@ export class ReactRouterViewStack extends ViewStacks {
713723
return false;
714724
}
715725

726+
// For empty path routes, only match if we're at the same level as when the view was created.
727+
// This prevents an empty path view item from being reused for different routes.
716728
if (isDefaultRoute) {
729+
const previousPathnameBase = v.routeData?.match?.pathnameBase || '';
730+
const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
731+
const normalizedPathname = normalizePathnameForComparison(pathname);
732+
733+
if (normalizedPathname !== normalizedBase) {
734+
return false;
735+
}
736+
717737
match = {
718738
params: {},
719739
pathname,

0 commit comments

Comments
 (0)