Skip to content

Commit 4f930d1

Browse files
fix: ensure unique matchId when optional and required param are on the same level (#4751)
1 parent d87ca5e commit 4f930d1

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed

packages/react-router/tests/optional-path-params.test.tsx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,157 @@ describe('React Router - Optional Path Parameters', () => {
225225
)
226226
})
227227

228+
describe('required and optional parameters on the same level', () => {
229+
async function setupTestRouter() {
230+
const rootRoute = createRootRoute()
231+
232+
const indexRoute = createRoute({
233+
getParentRoute: () => rootRoute,
234+
path: '/',
235+
component: () => {
236+
return (
237+
<div data-testid="index-route-component">
238+
<h1>index</h1>
239+
<Link
240+
data-testid="reports-optional-param-link"
241+
params={{ adminLevelId: 'asdf' }}
242+
to="/admin-levels/{-$adminLevelId}/reports"
243+
>
244+
navigate to reports with optional param
245+
</Link>
246+
</div>
247+
)
248+
},
249+
})
250+
251+
const adminLevelsRoutes = createRoute({
252+
getParentRoute: () => rootRoute,
253+
254+
path: 'admin-levels',
255+
component: () => (
256+
<div data-testid="admin-levels-route-component">
257+
<h1>admin-levels</h1>
258+
<Outlet />
259+
</div>
260+
),
261+
})
262+
263+
const requiredParamRoute = createRoute({
264+
getParentRoute: () => adminLevelsRoutes,
265+
path: '$adminLevelId',
266+
component: () => (
267+
<div data-testid="admin-levels-route-required-param-route-component">
268+
<h1>Required Param Route</h1>
269+
<Outlet />
270+
</div>
271+
),
272+
})
273+
274+
const requiredParamIndexRoute = createRoute({
275+
getParentRoute: () => requiredParamRoute,
276+
path: '/',
277+
component: () => (
278+
<div data-testid="admin-levels-route-required-param-index-route-component">
279+
<h1>Required Param Route Index</h1>
280+
</div>
281+
),
282+
})
283+
284+
const optionalParamRoute = createRoute({
285+
getParentRoute: () => adminLevelsRoutes,
286+
path: '{-$adminLevelId}',
287+
component: () => (
288+
<div data-testid="admin-levels-route-optional-param-route-component">
289+
<h1>Optional Param Route</h1>
290+
<Outlet />
291+
</div>
292+
),
293+
})
294+
295+
const reportsRoute = createRoute({
296+
getParentRoute: () => optionalParamRoute,
297+
path: 'reports',
298+
component: () => (
299+
<div data-testid="reports-route-component">
300+
<h1>Reports</h1>
301+
<Link
302+
data-testid="navigate-to-required-param-link"
303+
to="/admin-levels/$adminLevelId"
304+
params={{ adminLevelId: 'asdf' }}
305+
>
306+
navigate to required param route
307+
</Link>
308+
<Outlet />
309+
</div>
310+
),
311+
})
312+
313+
const router = createRouter({
314+
routeTree: rootRoute.addChildren([
315+
indexRoute,
316+
adminLevelsRoutes.addChildren([
317+
requiredParamRoute.addChildren([requiredParamIndexRoute]),
318+
optionalParamRoute.addChildren([reportsRoute]),
319+
]),
320+
]),
321+
})
322+
323+
render(<RouterProvider router={router} />)
324+
await act(() => router.load())
325+
return router
326+
}
327+
328+
it('direct visit', async () => {
329+
window.history.replaceState({}, '', 'admin-levels/asdf')
330+
await setupTestRouter()
331+
332+
expect(
333+
await screen.findByTestId(
334+
'admin-levels-route-required-param-route-component',
335+
),
336+
).toBeInTheDocument()
337+
expect(
338+
await screen.findByTestId(
339+
'admin-levels-route-required-param-index-route-component',
340+
),
341+
).toBeInTheDocument()
342+
})
343+
344+
it('client-side navigation', async () => {
345+
window.history.replaceState({}, '', '/')
346+
347+
await setupTestRouter()
348+
349+
expect(
350+
await screen.findByTestId('index-route-component'),
351+
).toBeInTheDocument()
352+
const reportsLink = await screen.findByTestId(
353+
'reports-optional-param-link',
354+
)
355+
fireEvent.click(reportsLink)
356+
357+
expect(
358+
await screen.findByTestId('reports-route-component'),
359+
).toBeInTheDocument()
360+
const requiredParamLink = await screen.findByTestId(
361+
'navigate-to-required-param-link',
362+
)
363+
fireEvent.click(requiredParamLink)
364+
365+
expect(
366+
await screen.findByTestId(
367+
'admin-levels-route-required-param-route-component',
368+
),
369+
).toBeInTheDocument()
370+
371+
expect(
372+
await screen.findByTestId(
373+
'admin-levels-route-required-param-index-route-component',
374+
),
375+
).toBeInTheDocument()
376+
})
377+
})
378+
228379
describe('Link component with optional parameters', () => {
229380
it('should generate correct href for optional parameters', async () => {
230381
const rootRoute = createRootRoute()

packages/router-core/src/path.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ export function interpolatePath({
454454
const value = encodeParam(segment.value)
455455
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
456456
}
457+
if (leaveWildcards) {
458+
return `${segmentPrefix}${key}${encodeParam(key) ?? ''}${segmentSuffix}`
459+
}
457460
return `${segmentPrefix}${encodeParam(key) ?? ''}${segmentSuffix}`
458461
}
459462

0 commit comments

Comments
 (0)