Skip to content

Commit 2b9fa96

Browse files
nlynzaadroduyemi
authored andcommitted
refactor(router-core): use original nextTo path when building location in its route form (TanStack#5851)
* fixes and tests * standardize params output for empty optional params * finalize leave params tests * leave wildcard is no longer required * remove leaveParams altogether * leave changes to optional param output handling for a separate PR * cleanup * resolve build errors and nitpick
1 parent 35a6157 commit 2b9fa96

File tree

5 files changed

+450
-26
lines changed

5 files changed

+450
-26
lines changed

packages/react-router/tests/Matches.test.tsx

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { expect, test } from 'vitest'
2-
import { fireEvent, render, screen } from '@testing-library/react'
1+
import { afterEach, describe, expect, test } from 'vitest'
2+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
3+
import { createMemoryHistory } from '@tanstack/history'
34
import {
45
Link,
56
Outlet,
@@ -8,6 +9,7 @@ import {
89
createRoute,
910
createRouter,
1011
isMatch,
12+
useMatchRoute,
1113
useMatches,
1214
} from '../src'
1315

@@ -130,6 +132,7 @@ test('should show pendingComponent of root route', async () => {
130132
},
131133
component: () => <div data-testId="root-content" />,
132134
})
135+
133136
const router = createRouter({
134137
routeTree: root,
135138
defaultPendingMs: 0,
@@ -141,3 +144,213 @@ test('should show pendingComponent of root route', async () => {
141144
expect(await rendered.findByTestId('root-pending')).toBeInTheDocument()
142145
expect(await rendered.findByTestId('root-content')).toBeInTheDocument()
143146
})
147+
148+
describe('matching on different param types', () => {
149+
const testCases = [
150+
{
151+
name: 'param with braces',
152+
path: '/$id',
153+
nav: '/1',
154+
params: { id: '1' },
155+
matchParams: { id: '1' },
156+
},
157+
{
158+
name: 'param without braces',
159+
path: '/{$id}',
160+
nav: '/2',
161+
params: { id: '2' },
162+
matchParams: { id: '2' },
163+
},
164+
{
165+
name: 'param with prefix',
166+
path: '/prefix-{$id}',
167+
nav: '/prefix-3',
168+
params: { id: '3' },
169+
matchParams: { id: '3' },
170+
},
171+
{
172+
name: 'param with suffix',
173+
path: '/{$id}-suffix',
174+
nav: '/4-suffix',
175+
params: { id: '4' },
176+
matchParams: { id: '4' },
177+
},
178+
{
179+
name: 'param with prefix and suffix',
180+
path: '/prefix-{$id}-suffix',
181+
nav: '/prefix-5-suffix',
182+
params: { id: '5' },
183+
matchParams: { id: '5' },
184+
},
185+
{
186+
name: 'wildcard with no braces',
187+
path: '/abc/$',
188+
nav: '/abc/6',
189+
params: { '*': '6', _splat: '6' },
190+
matchParams: { '*': '6', _splat: '6' },
191+
},
192+
{
193+
name: 'wildcard with braces',
194+
path: '/abc/{$}',
195+
nav: '/abc/7',
196+
params: { '*': '7', _splat: '7' },
197+
matchParams: { '*': '7', _splat: '7' },
198+
},
199+
{
200+
name: 'wildcard with prefix',
201+
path: '/abc/prefix{$}',
202+
nav: '/abc/prefix/8',
203+
params: { '*': '/8', _splat: '/8' },
204+
matchParams: { '*': '/8', _splat: '/8' },
205+
},
206+
{
207+
name: 'wildcard with suffix',
208+
path: '/abc/{$}suffix',
209+
nav: '/abc/9/suffix',
210+
params: { _splat: '9/', '*': '9/' },
211+
matchParams: { _splat: '9/', '*': '9/' },
212+
},
213+
{
214+
name: 'optional param with no prefix/suffix and value',
215+
path: '/abc/{-$id}/def',
216+
nav: '/abc/10/def',
217+
params: { id: '10' },
218+
matchParams: { id: '10' },
219+
},
220+
{
221+
name: 'optional param with no prefix/suffix and requiredParam and no value',
222+
path: '/abc/{-$id}/$foo/def',
223+
nav: '/abc/bar/def',
224+
params: { foo: 'bar' },
225+
matchParams: { foo: 'bar' },
226+
},
227+
{
228+
name: 'optional param with no prefix/suffix and requiredParam and value',
229+
path: '/abc/{-$id}/$foo/def',
230+
nav: '/abc/10/bar/def',
231+
params: { id: '10', foo: 'bar' },
232+
matchParams: { id: '10', foo: 'bar' },
233+
},
234+
{
235+
name: 'optional param with no prefix/suffix and no value',
236+
path: '/abc/{-$id}/def',
237+
nav: '/abc/def',
238+
params: {},
239+
matchParams: {},
240+
},
241+
{
242+
name: 'multiple optional params with no prefix/suffix and no value',
243+
path: '/{-$a}/{-$b}/{-$c}',
244+
nav: '/',
245+
params: {},
246+
matchParams: {},
247+
},
248+
{
249+
name: 'multiple optional params with no prefix/suffix and values',
250+
path: '/{-$a}/{-$b}/{-$c}',
251+
nav: '/foo/bar/qux',
252+
params: { a: 'foo', b: 'bar', c: 'qux' },
253+
matchParams: { a: 'foo', b: 'bar', c: 'qux' },
254+
},
255+
{
256+
name: 'multiple optional params with no prefix/suffix and mixed values',
257+
path: '/{-$a}/{-$b}/{-$c}',
258+
nav: '/foo/qux',
259+
params: { a: 'foo', b: 'qux' },
260+
matchParams: { a: 'foo', b: 'qux' },
261+
},
262+
{
263+
name: 'optional param with prefix and value',
264+
path: '/optional-{-$id}',
265+
nav: '/optional-12',
266+
params: { id: '12' },
267+
matchParams: { id: '12' },
268+
},
269+
{
270+
name: 'optional param with prefix and no value',
271+
path: '/optional-{-$id}',
272+
nav: '/optional-',
273+
params: {},
274+
matchParams: { id: '' },
275+
},
276+
{
277+
name: 'optional param with suffix and value',
278+
path: '/{-$id}-optional',
279+
nav: '/13-optional',
280+
params: { id: '13' },
281+
matchParams: { id: '13' },
282+
},
283+
{
284+
name: 'optional param with suffix and no value',
285+
path: '/{-$id}-optional',
286+
nav: '/-optional',
287+
params: {},
288+
matchParams: { id: '' },
289+
},
290+
{
291+
name: 'optional param with required param, prefix, suffix, wildcard and no value',
292+
path: `/$foo/a{-$id}-optional/$`,
293+
nav: '/bar/a-optional/qux',
294+
params: { foo: 'bar', id: '', _splat: 'qux', '*': 'qux' },
295+
matchParams: { foo: 'bar', id: '', _splat: 'qux', '*': 'qux' },
296+
},
297+
{
298+
name: 'optional param with required param, prefix, suffix, wildcard and value',
299+
path: `/$foo/a{-$id}-optional/$`,
300+
nav: '/bar/a14-optional/qux',
301+
params: { foo: 'bar', id: '14', _splat: 'qux', '*': 'qux' },
302+
matchParams: { foo: 'bar', id: '14', _splat: 'qux', '*': 'qux' },
303+
},
304+
]
305+
306+
afterEach(() => cleanup())
307+
test.each(testCases)(
308+
'$name',
309+
async ({ name, path, params, matchParams, nav }) => {
310+
const rootRoute = createRootRoute()
311+
312+
const Route = createRoute({
313+
getParentRoute: () => rootRoute,
314+
path,
315+
component: RouteComponent,
316+
})
317+
318+
function RouteComponent() {
319+
const routeParams = Route.useParams()
320+
const matchRoute = useMatchRoute()
321+
const matchRouteMatch = matchRoute({
322+
to: path,
323+
})
324+
325+
return (
326+
<div>
327+
<h1 data-testid="heading">{name}</h1>
328+
<div>
329+
Params{' '}
330+
<span data-testid="params">{JSON.stringify(routeParams)}</span>
331+
Matches{' '}
332+
<span data-testid="matches">
333+
{JSON.stringify(matchRouteMatch)}
334+
</span>
335+
</div>
336+
</div>
337+
)
338+
}
339+
340+
const router = createRouter({
341+
routeTree: rootRoute.addChildren([Route]),
342+
history: createMemoryHistory({ initialEntries: ['/'] }),
343+
})
344+
345+
await act(() => render(<RouterProvider router={router} />))
346+
347+
act(() => router.history.push(nav))
348+
349+
const paramsToCheck = await screen.findByTestId('params')
350+
const matchesToCheck = await screen.findByTestId('matches')
351+
352+
expect(JSON.parse(paramsToCheck.textContent)).toEqual(params)
353+
expect(JSON.parse(matchesToCheck.textContent)).toEqual(matchParams)
354+
},
355+
)
356+
})

packages/router-core/src/path.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,6 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
377377
interface InterpolatePathOptions {
378378
path?: string
379379
params: Record<string, unknown>
380-
leaveParams?: boolean
381380
// Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
382381
decodeCharMap?: Map<string, string>
383382
parseCache?: ParsePathnameCache
@@ -393,7 +392,6 @@ type InterPolatePathResult = {
393392
*
394393
* - Encodes params safely (configurable allowed characters)
395394
* - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards
396-
* - Optionally leaves placeholders or wildcards in place
397395
*/
398396
/**
399397
* Interpolate params and wildcards into a route path template.
@@ -402,7 +400,6 @@ type InterPolatePathResult = {
402400
export function interpolatePath({
403401
path,
404402
params,
405-
leaveParams,
406403
decodeCharMap,
407404
parseCache,
408405
}: InterpolatePathOptions): InterPolatePathResult {
@@ -452,6 +449,7 @@ export function interpolatePath({
452449
}
453450

454451
const value = encodeParam('_splat')
452+
455453
return `${segmentPrefix}${value}${segmentSuffix}`
456454
}
457455

@@ -464,10 +462,7 @@ export function interpolatePath({
464462

465463
const segmentPrefix = segment.prefixSegment || ''
466464
const segmentSuffix = segment.suffixSegment || ''
467-
if (leaveParams) {
468-
const value = encodeParam(segment.value)
469-
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
470-
}
465+
471466
return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
472467
}
473468

@@ -489,10 +484,6 @@ export function interpolatePath({
489484

490485
usedParams[key] = params[key]
491486

492-
if (leaveParams) {
493-
const value = encodeParam(segment.value)
494-
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
495-
}
496487
return `${segmentPrefix}${encodeParam(key) ?? ''}${segmentSuffix}`
497488
}
498489

packages/router-core/src/router.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,17 +1683,18 @@ export class RouterCore<
16831683
}
16841684
}
16851685

1686-
const nextPathname = decodePath(
1687-
interpolatePath({
1688-
// Use the original template path for interpolation
1686+
const nextPathname = opts.leaveParams
1687+
? // Use the original template path for interpolation
16891688
// This preserves the original parameter syntax including optional parameters
1690-
path: nextTo,
1691-
params: nextParams,
1692-
leaveParams: opts.leaveParams,
1693-
decodeCharMap: this.pathParamsDecodeCharMap,
1694-
parseCache: this.parsePathnameCache,
1695-
}).interpolatedPath,
1696-
)
1689+
nextTo
1690+
: decodePath(
1691+
interpolatePath({
1692+
path: nextTo,
1693+
params: nextParams,
1694+
decodeCharMap: this.pathParamsDecodeCharMap,
1695+
parseCache: this.parsePathnameCache,
1696+
}).interpolatedPath,
1697+
)
16971698

16981699
// Resolve the next search
16991700
let nextSearch = fromSearch

packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ function RouteComp({
178178
const interpolated = interpolatePath({
179179
path: route.fullPath,
180180
params: allParams,
181-
leaveParams: false,
182181
decodeCharMap: router().pathParamsDecodeCharMap,
183182
})
184183

0 commit comments

Comments
 (0)