From b95432023b7f1223c6c985b6f55c95a862215800 Mon Sep 17 00:00:00 2001 From: Alexandra de Groof Date: Wed, 17 Jul 2024 07:38:19 +0200 Subject: [PATCH 1/5] Fix support for optional static segments in matchPath and added tests --- .../react-router/__tests__/matchPath-test.tsx | 76 ++++++++++++++++++- packages/react-router/lib/router/utils.ts | 3 +- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/matchPath-test.tsx b/packages/react-router/__tests__/matchPath-test.tsx index 5d4a95b0aa..e3ae0ffedd 100644 --- a/packages/react-router/__tests__/matchPath-test.tsx +++ b/packages/react-router/__tests__/matchPath-test.tsx @@ -245,7 +245,7 @@ describe("matchPath", () => { }); }); -describe("matchPath optional segments", () => { +describe("matchPath optional dynamic segments", () => { it("should match when optional segment is provided", () => { const match = matchPath("/:lang?/user/:id", "/en/user/123"); expect(match).toMatchObject({ params: { lang: "en", id: "123" } }); @@ -292,6 +292,80 @@ describe("matchPath optional segments", () => { }); }); +describe("matchPath optional static segments", () => { + it("should match when optional segment is provided", () => { + const match = matchPath("/school?/user/:id", "/school/user/123"); + expect(match).toMatchObject({ + pathname: "/school/user/123", + pathnameBase: "/school/user/123", + }); + }); + + it("should match when optional segment is *not* provided", () => { + const match = matchPath("/school?/user/:id", "/user/123"); + expect(match).toMatchObject({ + pathname: "/user/123", + pathnameBase: "/user/123", + }); + }); + + it("should match when middle optional segment is provided", () => { + const match = matchPath("/school/user?/:id", "/school/user/123"); + expect(match).toMatchObject({ + pathname: "/school/user/123", + pathnameBase: "/school/user/123", + }); + }); + + it("should match when middle optional segment is *not* provided", () => { + const match = matchPath("/school/user?/:id", "/school/123"); + expect(match).toMatchObject({ + pathname: "/school/123", + pathnameBase: "/school/123", + }); + }); + + it("should match when end optional segment is provided", () => { + const match = matchPath("/school/user/admin?", "/school/user/admin"); + expect(match).toMatchObject({ + pathname: "/school/user/admin", + pathnameBase: "/school/user/admin", + }); + }); + + it("should match when end optional segment is *not* provided", () => { + const match = matchPath("/school/user/admin?", "/school/user"); + expect(match).toMatchObject({ + pathname: "/school/user", + pathnameBase: "/school/user", + }); + }); + + it("should match multiple optional segments and none are provided", () => { + const match = matchPath("/school?/user/admin?", "/user"); + expect(match).toMatchObject({ + pathname: "/user", + pathnameBase: "/user", + }); + }); + + it("should match multiple optional segments and one is provided", () => { + const match = matchPath("/school?/user/admin?", "/user/admin"); + expect(match).toMatchObject({ + pathname: "/user/admin", + pathnameBase: "/user/admin", + }); + }); + + it("should match multiple optional segments and all are provided", () => { + const match = matchPath("/school?/user/admin?", "/school/user/admin"); + expect(match).toMatchObject({ + pathname: "/school/user/admin", + pathnameBase: "/school/user/admin", + }); + }); +}); + describe("matchPath *", () => { it("matches the root URL", () => { expect(matchPath("*", "/")).toMatchObject({ diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 07db268acf..4d0f99d855 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1014,7 +1014,8 @@ function compilePath( params.push({ paramName, isOptional: isOptional != null }); return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)"; } - ); + ) // Dynamic segment + .replace(/\/([\w-]+)\?/g, "(/$1)?"); // Optional static segment if (path.endsWith("*")) { params.push({ paramName: "*" }); From 5ef7ea8b5499b9a8abe3b939f0457b8bc566a30a Mon Sep 17 00:00:00 2001 From: Alexandra de Groof Date: Wed, 17 Jul 2024 10:06:20 +0200 Subject: [PATCH 2/5] Updated contributors.yml --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index ea24af9bbd..2bdd8427a6 100644 --- a/contributors.yml +++ b/contributors.yml @@ -152,6 +152,7 @@ - KubasuIvanSakwa - KutnerUri - kylegirard +- LadyTsukiko - landisdesign - latin-1 - lequangdongg From 2a801a7f75065c008225218557a6307eec495a45 Mon Sep 17 00:00:00 2001 From: Alexandra de Groof Date: Wed, 17 Jul 2024 10:38:48 +0200 Subject: [PATCH 3/5] Added changeset --- .changeset/eleven-humans-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eleven-humans-smell.md diff --git a/.changeset/eleven-humans-smell.md b/.changeset/eleven-humans-smell.md new file mode 100644 index 0000000000..cb65984994 --- /dev/null +++ b/.changeset/eleven-humans-smell.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +fixed optional static segment matching From 96add192ef76a34ba717a52e6bd2b8ee28f7e94e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 6 Aug 2025 11:22:43 -0400 Subject: [PATCH 4/5] Update .changeset/eleven-humans-smell.md --- .changeset/eleven-humans-smell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eleven-humans-smell.md b/.changeset/eleven-humans-smell.md index cb65984994..5b2d33cdb9 100644 --- a/.changeset/eleven-humans-smell.md +++ b/.changeset/eleven-humans-smell.md @@ -2,4 +2,4 @@ "react-router": patch --- -fixed optional static segment matching +Fix optional static segment matching in `matchPath` From e6ea20f0208588725183203508dc50082451c237 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 6 Aug 2025 14:55:11 -0400 Subject: [PATCH 5/5] Dont match question marks in the middle of the static string --- packages/react-router/__tests__/matchPath-test.tsx | 7 +++++++ packages/react-router/lib/router/utils.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-router/__tests__/matchPath-test.tsx b/packages/react-router/__tests__/matchPath-test.tsx index 28d9b76d9c..338b8b7320 100644 --- a/packages/react-router/__tests__/matchPath-test.tsx +++ b/packages/react-router/__tests__/matchPath-test.tsx @@ -364,6 +364,13 @@ describe("matchPath optional static segments", () => { pathnameBase: "/school/user/admin", }); }); + + it("does not trigger from question marks in the middle of the optional static segment", () => { + let match = matchPath("/school?abc/user/:id", "/abc/user/123"); + expect(match).toBe(null); + match = matchPath("/school?abc", "/abc"); + expect(match).toBe(null); + }); }); describe("matchPath *", () => { diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index d284341af8..107e323e03 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1445,7 +1445,7 @@ export function compilePath( return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)"; }, ) // Dynamic segment - .replace(/\/([\w-]+)\?/g, "(/$1)?"); // Optional static segment + .replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2"); // Optional static segment if (path.endsWith("*")) { params.push({ paramName: "*" });