Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-humans-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Fix optional static segment matching in `matchPath`
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
- KubasuIvanSakwa
- KutnerUri
- kylegirard
- LadyTsukiko
- landisdesign
- latin-1
- lazybean
Expand Down
76 changes: 75 additions & 1 deletion packages/react-router/__tests__/matchPath-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" } });
Expand Down Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion packages/react-router/lib/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,8 @@ export 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: "*" });
Expand Down