From dd5a1760412e0d65bd653fbc27d27d5cf41f6c43 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 6 Aug 2025 14:41:11 -0400 Subject: [PATCH 1/2] Add support for optional segmnts in nested absolute routes --- .../__tests__/path-matching-test.tsx | 70 +++++++++++++++++++ packages/react-router/lib/router/utils.ts | 23 +++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx index d13c3f0bf6..337b99673d 100644 --- a/packages/react-router/__tests__/path-matching-test.tsx +++ b/packages/react-router/__tests__/path-matching-test.tsx @@ -487,6 +487,76 @@ describe("path matching with optional segments", () => { { path: "abc", params: {} }, ]); }); + + test("optional static segments in nested absolute routes (leading)", () => { + let nested = [ + { + path: "/en?", + children: [ + { + path: "/en?/abc", + children: [ + { + path: "/en?/abc/def", + }, + ], + }, + ], + }, + ]; + + expect(pickPathsAndParams(nested, "/en/abc")).toEqual([ + { path: "/en?", params: {} }, + { path: "/en?/abc", params: {} }, + ]); + expect(pickPathsAndParams(nested, "/abc")).toEqual([ + { path: "/en?", params: {} }, + { path: "/en?/abc", params: {} }, + ]); + expect(pickPathsAndParams(nested, "/en/abc/def")).toEqual([ + { path: "/en?", params: {} }, + { path: "/en?/abc", params: {} }, + { path: "/en?/abc/def", params: {} }, + ]); + expect(pickPathsAndParams(nested, "/abc/def")).toEqual([ + { path: "/en?", params: {} }, + { path: "/en?/abc", params: {} }, + { path: "/en?/abc/def", params: {} }, + ]); + }); + + test("optional static segment in nested absolute routes (middle)", () => { + let nested = [ + { + path: "/en", + children: [ + { + path: "/en/abc?", + children: [ + { + path: "/en/abc?/def", + }, + ], + }, + ], + }, + ]; + + expect(pickPathsAndParams(nested, "/en/abc")).toEqual([ + { path: "/en", params: {} }, + { path: "/en/abc?", params: {} }, + ]); + expect(pickPathsAndParams(nested, "/en/abc/def")).toEqual([ + { path: "/en", params: {} }, + { path: "/en/abc?", params: {} }, + { path: "/en/abc?/def", params: {} }, + ]); + expect(pickPathsAndParams(nested, "/en/def")).toEqual([ + { path: "/en", params: {} }, + { path: "/en/abc?", params: {} }, + { path: "/en/abc?/def", params: {} }, + ]); + }); }); describe("path matching with optional dynamic segments", () => { diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 0ffdf25f16..80e783a4d2 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -982,10 +982,12 @@ function flattenRoutes< branches: RouteBranch[] = [], parentsMeta: RouteMeta[] = [], parentPath = "", + _hasParentOptionalSegments = false, ): RouteBranch[] { let flattenRoute = ( route: RouteObjectType, index: number, + hasParentOptionalSegments = _hasParentOptionalSegments, relativePath?: string, ) => { let meta: RouteMeta = { @@ -997,6 +999,17 @@ function flattenRoutes< }; if (meta.relativePath.startsWith("/")) { + if ( + !meta.relativePath.startsWith(parentPath) && + hasParentOptionalSegments + ) { + // If we're inside of a parent route that has optional segments, we don't + // want to throw a hard error here because due to the route exploding + // approach, some of the routes won't match by design and we can just + // discard them instead. + // https://github.com/remix-run/react-router/issues/9925#issuecomment-1387252214 + return; + } invariant( meta.relativePath.startsWith(parentPath), `Absolute route path "${meta.relativePath}" nested under path ` + @@ -1021,7 +1034,13 @@ function flattenRoutes< `Index routes must not have child routes. Please remove ` + `all child routes from route path "${path}".`, ); - flattenRoutes(route.children, branches, routesMeta, path); + flattenRoutes( + route.children, + branches, + routesMeta, + path, + hasParentOptionalSegments, + ); } // Routes without a path shouldn't ever match by themselves unless they are @@ -1042,7 +1061,7 @@ function flattenRoutes< flattenRoute(route, index); } else { for (let exploded of explodeOptionalSegments(route.path)) { - flattenRoute(route, index, exploded); + flattenRoute(route, index, true, exploded); } } }); From 36c9dee76999bbfbc86ddbb5da257a8a89ca0c6d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 6 Aug 2025 16:34:01 -0400 Subject: [PATCH 2/2] Add changeset --- .changeset/bright-bats-wave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-bats-wave.md diff --git a/.changeset/bright-bats-wave.md b/.changeset/bright-bats-wave.md new file mode 100644 index 0000000000..9ec9ffc905 --- /dev/null +++ b/.changeset/bright-bats-wave.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix usage of optional path segments in nested routes defined using absolute paths