Skip to content

Commit bb4a55e

Browse files
authored
Fix support for optional static segments (#11813)
1 parent f7bc402 commit bb4a55e

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed

.changeset/eleven-humans-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix optional static segment matching in `matchPath`

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
- KubasuIvanSakwa
213213
- KutnerUri
214214
- kylegirard
215+
- LadyTsukiko
215216
- landisdesign
216217
- latin-1
217218
- lazybean

packages/react-router/__tests__/matchPath-test.tsx

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ describe("matchPath", () => {
245245
});
246246
});
247247

248-
describe("matchPath optional segments", () => {
248+
describe("matchPath optional dynamic segments", () => {
249249
it("should match when optional segment is provided", () => {
250250
const match = matchPath("/:lang?/user/:id", "/en/user/123");
251251
expect(match).toMatchObject({ params: { lang: "en", id: "123" } });
@@ -292,6 +292,87 @@ describe("matchPath optional segments", () => {
292292
});
293293
});
294294

295+
describe("matchPath optional static segments", () => {
296+
it("should match when optional segment is provided", () => {
297+
const match = matchPath("/school?/user/:id", "/school/user/123");
298+
expect(match).toMatchObject({
299+
pathname: "/school/user/123",
300+
pathnameBase: "/school/user/123",
301+
});
302+
});
303+
304+
it("should match when optional segment is *not* provided", () => {
305+
const match = matchPath("/school?/user/:id", "/user/123");
306+
expect(match).toMatchObject({
307+
pathname: "/user/123",
308+
pathnameBase: "/user/123",
309+
});
310+
});
311+
312+
it("should match when middle optional segment is provided", () => {
313+
const match = matchPath("/school/user?/:id", "/school/user/123");
314+
expect(match).toMatchObject({
315+
pathname: "/school/user/123",
316+
pathnameBase: "/school/user/123",
317+
});
318+
});
319+
320+
it("should match when middle optional segment is *not* provided", () => {
321+
const match = matchPath("/school/user?/:id", "/school/123");
322+
expect(match).toMatchObject({
323+
pathname: "/school/123",
324+
pathnameBase: "/school/123",
325+
});
326+
});
327+
328+
it("should match when end optional segment is provided", () => {
329+
const match = matchPath("/school/user/admin?", "/school/user/admin");
330+
expect(match).toMatchObject({
331+
pathname: "/school/user/admin",
332+
pathnameBase: "/school/user/admin",
333+
});
334+
});
335+
336+
it("should match when end optional segment is *not* provided", () => {
337+
const match = matchPath("/school/user/admin?", "/school/user");
338+
expect(match).toMatchObject({
339+
pathname: "/school/user",
340+
pathnameBase: "/school/user",
341+
});
342+
});
343+
344+
it("should match multiple optional segments and none are provided", () => {
345+
const match = matchPath("/school?/user/admin?", "/user");
346+
expect(match).toMatchObject({
347+
pathname: "/user",
348+
pathnameBase: "/user",
349+
});
350+
});
351+
352+
it("should match multiple optional segments and one is provided", () => {
353+
const match = matchPath("/school?/user/admin?", "/user/admin");
354+
expect(match).toMatchObject({
355+
pathname: "/user/admin",
356+
pathnameBase: "/user/admin",
357+
});
358+
});
359+
360+
it("should match multiple optional segments and all are provided", () => {
361+
const match = matchPath("/school?/user/admin?", "/school/user/admin");
362+
expect(match).toMatchObject({
363+
pathname: "/school/user/admin",
364+
pathnameBase: "/school/user/admin",
365+
});
366+
});
367+
368+
it("does not trigger from question marks in the middle of the optional static segment", () => {
369+
let match = matchPath("/school?abc/user/:id", "/abc/user/123");
370+
expect(match).toBe(null);
371+
match = matchPath("/school?abc", "/abc");
372+
expect(match).toBe(null);
373+
});
374+
});
375+
295376
describe("matchPath *", () => {
296377
it("matches the root URL", () => {
297378
expect(matchPath("*", "/")).toMatchObject({

packages/react-router/lib/router/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,8 @@ export function compilePath(
14441444
params.push({ paramName, isOptional: isOptional != null });
14451445
return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
14461446
},
1447-
);
1447+
) // Dynamic segment
1448+
.replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2"); // Optional static segment
14481449

14491450
if (path.endsWith("*")) {
14501451
params.push({ paramName: "*" });

0 commit comments

Comments
 (0)