Skip to content

Commit 30f6c1d

Browse files
authored
fix(react-router): handle parameters with static suffixes in generatePath (#14269)
1 parent 7f140e0 commit 30f6c1d

4 files changed

Lines changed: 106 additions & 4 deletions

File tree

.changeset/beige-wasps-cover.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 `generatePath` when used with suffixed params (i.e., "/books/:id.json")

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,25 @@ describe("generatePath", () => {
191191

192192
consoleWarn.mockRestore();
193193
});
194+
195+
describe("with params followed by static text", () => {
196+
it("interpolates params with file extensions", () => {
197+
expect(generatePath("/books/:id.json", { id: "42" })).toBe(
198+
"/books/42.json",
199+
);
200+
expect(generatePath("/api/:resource.xml", { resource: "users" })).toBe(
201+
"/api/users.xml",
202+
);
203+
expect(generatePath("/:lang.html", { lang: "en" })).toBe("/en.html");
204+
});
205+
206+
it("handles multiple extensions", () => {
207+
expect(generatePath("/files/:name.tar.gz", { name: "archive" })).toBe(
208+
"/files/archive.tar.gz",
209+
);
210+
expect(generatePath("/:file.min.js", { file: "app" })).toBe(
211+
"/app.min.js",
212+
);
213+
});
214+
});
194215
});

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

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import * as React from "react";
22
import * as TestRenderer from "react-test-renderer";
3-
import { MemoryRouter, Outlet, Routes, Route, useParams } from "react-router";
3+
import {
4+
MemoryRouter,
5+
Outlet,
6+
Routes,
7+
Route,
8+
useParams,
9+
useLocation,
10+
generatePath,
11+
} from "react-router";
412

513
function ShowParams() {
614
return <pre>{JSON.stringify(useParams())}</pre>;
@@ -209,4 +217,72 @@ describe("useParams", () => {
209217
`);
210218
});
211219
});
220+
221+
test("maintains compatibility with generatePath", () => {
222+
let tests = [
223+
{
224+
path: "/books/42",
225+
url: "/books/42",
226+
params: {},
227+
},
228+
{
229+
path: "/books/:id",
230+
url: "/books/42",
231+
params: { id: "42" },
232+
},
233+
{
234+
path: "/books/:id.json",
235+
url: "/books/42.json",
236+
params: { id: "42" },
237+
},
238+
{
239+
path: "/books/:id/comments",
240+
url: "/books/42/comments",
241+
params: { id: "42" },
242+
},
243+
{
244+
path: "/books/:id.json/comments",
245+
url: "/books/42.json/comments",
246+
params: { id: "42" },
247+
},
248+
];
249+
250+
function ShowParamsAndPath({ path }: { path: string }) {
251+
return (
252+
<>
253+
<p>{JSON.stringify(useParams())}</p>
254+
<p>{useLocation().pathname}</p>
255+
<p>{generatePath(path, useParams())}</p>
256+
</>
257+
);
258+
}
259+
260+
for (let { path, url, params } of tests) {
261+
let renderer: TestRenderer.ReactTestRenderer;
262+
TestRenderer.act(() => {
263+
renderer = TestRenderer.create(
264+
<MemoryRouter initialEntries={[url]}>
265+
<Routes>
266+
<Route path={path} element={<ShowParamsAndPath path={path} />} />
267+
</Routes>
268+
</MemoryRouter>,
269+
);
270+
});
271+
272+
// eslint-disable-next-line jest/no-interpolation-in-snapshots
273+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
274+
[
275+
<p>
276+
${JSON.stringify(params)}
277+
</p>,
278+
<p>
279+
${url}
280+
</p>,
281+
<p>
282+
${url}
283+
</p>,
284+
]
285+
`);
286+
}
287+
});
212288
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,12 +1337,12 @@ export function generatePath<Path extends string>(
13371337
return stringify(params[star]);
13381338
}
13391339

1340-
const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
1340+
const keyMatch = segment.match(/^:([\w-]+)(\??)(.*)/);
13411341
if (keyMatch) {
1342-
const [, key, optional] = keyMatch;
1342+
const [, key, optional, suffix] = keyMatch;
13431343
let param = params[key as PathParam<Path>];
13441344
invariant(optional === "?" || param != null, `Missing ":${key}" param`);
1345-
return encodeURIComponent(stringify(param));
1345+
return encodeURIComponent(stringify(param)) + suffix;
13461346
}
13471347

13481348
// Remove any optional markers from optional static segments

0 commit comments

Comments
 (0)