diff --git a/e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts b/e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts
index 8c0e0e1a1ec..dccdfd4acf2 100644
--- a/e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test'
-import { useExperimentalNonNestedRoutes } from './utils/useExperimentalNonNestedRoutes'
const testCases: Array<{
name: string
@@ -185,13 +184,7 @@ test.describe('Non-nested paths', () => {
await expect(pathRouteHeading).not.toBeVisible()
await expect(barHeading).toBeVisible()
const bar2ParamValue = await barParams.innerText()
- if (useExperimentalNonNestedRoutes || testPathDesc !== 'named') {
- expect(JSON.parse(bar2ParamValue)).toEqual(paramValue2)
- } else {
- // this is a bug with named path params and non-nested paths
- // that is resolved in the new experimental flag
- expect(JSON.parse(bar2ParamValue)).toEqual(paramValue)
- }
+ expect(JSON.parse(bar2ParamValue)).toEqual(paramValue2)
})
})
},
@@ -350,17 +343,8 @@ test.describe('Deeply nested non-nested paths', () => {
await expect(bazBarFooRouteHeading).not.toBeVisible()
await expect(bazBarFooQuxHeading).toBeVisible()
await expect(bazBarFooQuxParams).toBeVisible()
-
- if (useExperimentalNonNestedRoutes) {
- expect(await bazBarFooQuxParams.innerText()).toBe(
- JSON.stringify({ baz: 'baz-bar-qux', foo: 'foo' }),
- )
- } else {
- // this is a bug with named path params and non-nested paths
- // that is resolved in the new experimental flag
- expect(await bazBarFooQuxParams.innerText()).toBe(
- JSON.stringify({ baz: 'baz-bar', foo: 'foo' }),
- )
- }
+ expect(await bazBarFooQuxParams.innerText()).toBe(
+ JSON.stringify({ baz: 'baz-bar-qux', foo: 'foo' }),
+ )
})
})
diff --git a/e2e/react-router/basic-file-based/tests/params.spec.ts b/e2e/react-router/basic-file-based/tests/params.spec.ts
index c05dae91e05..6b43dba314e 100644
--- a/e2e/react-router/basic-file-based/tests/params.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/params.spec.ts
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test'
-import { useExperimentalNonNestedRoutes } from './utils/useExperimentalNonNestedRoutes'
import type { Page } from '@playwright/test'
test.beforeEach(async ({ page }) => {
@@ -100,22 +99,12 @@ test.describe('params operations + non-nested routes', () => {
const foo2ParamsValue = page.getByTestId('foo-params-value')
const foo2ParamsText = await foo2ParamsValue.innerText()
const foo2ParamsObj = JSON.parse(foo2ParamsText)
- if (useExperimentalNonNestedRoutes) {
- expect(foo2ParamsObj).toEqual({ foo: 'foo2' })
- } else {
- // this is a bug that is resolved in the new experimental flag
- expect(foo2ParamsObj).toEqual({ foo: 'foo' })
- }
+ expect(foo2ParamsObj).toEqual({ foo: 'foo2' })
const params2Value = page.getByTestId('foo-bar-params-value')
const params2Text = await params2Value.innerText()
const params2Obj = JSON.parse(params2Text)
- if (useExperimentalNonNestedRoutes) {
- expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' })
- } else {
- // this is a bug that is resolved in the new experimental flag
- expect(params2Obj).toEqual({ foo: 'foo', bar: 'bar2' })
- }
+ expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' })
})
})
diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts
index a175e785899..ea5bc50a072 100644
--- a/e2e/react-start/basic/tests/navigation.spec.ts
+++ b/e2e/react-start/basic/tests/navigation.spec.ts
@@ -9,6 +9,7 @@ test.use({
})
test('Navigating to post', async ({ page }) => {
await page.goto('/')
+ await page.waitForURL('/')
await page.getByRole('link', { name: 'Posts' }).click()
await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
diff --git a/e2e/solid-router/basic-file-based/tests/non-nested-paths.spec.ts b/e2e/solid-router/basic-file-based/tests/non-nested-paths.spec.ts
index 8c0e0e1a1ec..dccdfd4acf2 100644
--- a/e2e/solid-router/basic-file-based/tests/non-nested-paths.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/non-nested-paths.spec.ts
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test'
-import { useExperimentalNonNestedRoutes } from './utils/useExperimentalNonNestedRoutes'
const testCases: Array<{
name: string
@@ -185,13 +184,7 @@ test.describe('Non-nested paths', () => {
await expect(pathRouteHeading).not.toBeVisible()
await expect(barHeading).toBeVisible()
const bar2ParamValue = await barParams.innerText()
- if (useExperimentalNonNestedRoutes || testPathDesc !== 'named') {
- expect(JSON.parse(bar2ParamValue)).toEqual(paramValue2)
- } else {
- // this is a bug with named path params and non-nested paths
- // that is resolved in the new experimental flag
- expect(JSON.parse(bar2ParamValue)).toEqual(paramValue)
- }
+ expect(JSON.parse(bar2ParamValue)).toEqual(paramValue2)
})
})
},
@@ -350,17 +343,8 @@ test.describe('Deeply nested non-nested paths', () => {
await expect(bazBarFooRouteHeading).not.toBeVisible()
await expect(bazBarFooQuxHeading).toBeVisible()
await expect(bazBarFooQuxParams).toBeVisible()
-
- if (useExperimentalNonNestedRoutes) {
- expect(await bazBarFooQuxParams.innerText()).toBe(
- JSON.stringify({ baz: 'baz-bar-qux', foo: 'foo' }),
- )
- } else {
- // this is a bug with named path params and non-nested paths
- // that is resolved in the new experimental flag
- expect(await bazBarFooQuxParams.innerText()).toBe(
- JSON.stringify({ baz: 'baz-bar', foo: 'foo' }),
- )
- }
+ expect(await bazBarFooQuxParams.innerText()).toBe(
+ JSON.stringify({ baz: 'baz-bar-qux', foo: 'foo' }),
+ )
})
})
diff --git a/e2e/solid-router/basic-file-based/tests/params.spec.ts b/e2e/solid-router/basic-file-based/tests/params.spec.ts
index 9d57f437a72..ca9b2ca4292 100644
--- a/e2e/solid-router/basic-file-based/tests/params.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/params.spec.ts
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test'
-import { useExperimentalNonNestedRoutes } from './utils/useExperimentalNonNestedRoutes'
import type { Page } from '@playwright/test'
test.beforeEach(async ({ page }) => {
@@ -100,22 +99,12 @@ test.describe('params operations + non-nested routes', () => {
const foo2ParamsValue = page.getByTestId('foo-params-value')
const foo2ParamsText = await foo2ParamsValue.innerText()
const foo2ParamsObj = JSON.parse(foo2ParamsText)
- if (useExperimentalNonNestedRoutes) {
- expect(foo2ParamsObj).toEqual({ foo: 'foo2' })
- } else {
- // this is a bug that is resolved in the new experimental flag
- expect(foo2ParamsObj).toEqual({ foo: 'foo' })
- }
+ expect(foo2ParamsObj).toEqual({ foo: 'foo2' })
const params2Value = page.getByTestId('foo-bar-params-value')
const params2Text = await params2Value.innerText()
const params2Obj = JSON.parse(params2Text)
- if (useExperimentalNonNestedRoutes) {
- expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' })
- } else {
- // this is a bug that is resolved in the new experimental flag
- expect(params2Obj).toEqual({ foo: 'foo', bar: 'bar2' })
- }
+ expect(params2Obj).toEqual({ foo: 'foo2', bar: 'bar2' })
})
})
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index f19bf2328f5..6c16c330b56 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -355,7 +355,7 @@ export const Outlet = React.memo(function OutletImpl() {
const nextMatch =
- if (matchId === rootRouteId) {
+ if (routeId === rootRouteId) {
return (
{nextMatch}
)
diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts
index d0e22d6c9a0..234be73df95 100644
--- a/packages/router-core/src/load-matches.ts
+++ b/packages/router-core/src/load-matches.ts
@@ -213,7 +213,7 @@ const isBeforeLoadSsr = (
// in SPA mode, only SSR the root route
if (inner.router.isShell()) {
- existingMatch.ssr = matchId === rootRouteId
+ existingMatch.ssr = route.id === rootRouteId
return
}
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index 6b97f944bfa..e919d367cc1 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -377,7 +377,6 @@ function baseParsePathname(pathname: string): ReadonlyArray {
interface InterpolatePathOptions {
path?: string
params: Record
- leaveWildcards?: boolean
leaveParams?: boolean
// Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
decodeCharMap?: Map
@@ -403,7 +402,6 @@ type InterPolatePathResult = {
export function interpolatePath({
path,
params,
- leaveWildcards,
leaveParams,
decodeCharMap,
parseCache,
@@ -446,9 +444,6 @@ export function interpolatePath({
if (!params._splat) {
isMissingParams = true
// For missing splat parameters, just return the prefix and suffix without the wildcard
- if (leaveWildcards) {
- return `${segmentPrefix}${segment.value}${segmentSuffix}`
- }
// If there is a prefix or suffix, return them joined, otherwise omit the segment
if (segmentPrefix || segmentSuffix) {
return `${segmentPrefix}${segmentSuffix}`
@@ -457,9 +452,6 @@ export function interpolatePath({
}
const value = encodeParam('_splat')
- if (leaveWildcards) {
- return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
- }
return `${segmentPrefix}${value}${segmentSuffix}`
}
@@ -487,9 +479,6 @@ export function interpolatePath({
// Check if optional parameter is missing or undefined
if (!(key in params) || params[key] == null) {
- if (leaveWildcards) {
- return `${segmentPrefix}${key}${segmentSuffix}`
- }
// For optional params with prefix/suffix, keep the prefix/suffix but omit the param
if (segmentPrefix || segmentSuffix) {
return `${segmentPrefix}${segmentSuffix}`
@@ -504,9 +493,6 @@ export function interpolatePath({
const value = encodeParam(segment.value)
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
}
- if (leaveWildcards) {
- return `${segmentPrefix}${key}${encodeParam(key) ?? ''}${segmentSuffix}`
- }
return `${segmentPrefix}${encodeParam(key) ?? ''}${segmentSuffix}`
}
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index a29a07a6889..962c536045a 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1364,13 +1364,12 @@ export class RouterCore<
// Existing matches are matches that are already loaded along with
// pending matches that are still loading
const matchId =
- interpolatePath({
- path: route.id,
- params: routeParams,
- leaveWildcards: true,
- decodeCharMap: this.pathParamsDecodeCharMap,
- parseCache: this.parsePathnameCache,
- }).interpolatedPath + loaderDepsHash
+ // route.id for disambiguation
+ route.id +
+ // interpolatedPath for param changes
+ interpolatedPath +
+ // explicit deps
+ loaderDepsHash
const existingMatch = this.getMatch(matchId)
@@ -1690,7 +1689,6 @@ export class RouterCore<
// This preserves the original parameter syntax including optional parameters
path: nextTo,
params: nextParams,
- leaveWildcards: false,
leaveParams: opts.leaveParams,
decodeCharMap: this.pathParamsDecodeCharMap,
parseCache: this.parsePathnameCache,
diff --git a/packages/router-core/tests/callbacks.test.ts b/packages/router-core/tests/callbacks.test.ts
index 13d65d98aaf..b6a5343eb66 100644
--- a/packages/router-core/tests/callbacks.test.ts
+++ b/packages/router-core/tests/callbacks.test.ts
@@ -48,14 +48,14 @@ describe('callbacks', () => {
await router.navigate({ to: '/foo' })
expect(onEnter).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ id: '/foo' }),
+ expect.objectContaining({ id: '/foo/foo' }),
)
// Entering bar
await router.navigate({ to: '/bar' })
expect(onEnter).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ id: '/bar' }),
+ expect.objectContaining({ id: '/bar/bar' }),
)
})
})
@@ -70,14 +70,14 @@ describe('callbacks', () => {
await router.navigate({ to: '/bar' })
expect(onLeave).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ id: '/foo' }),
+ expect.objectContaining({ id: '/foo/foo' }),
)
// Leaving bar to foo
await router.navigate({ to: '/foo' })
expect(onLeave).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ id: '/bar' }),
+ expect.objectContaining({ id: '/bar/bar' }),
)
})
})
@@ -92,14 +92,14 @@ describe('callbacks', () => {
await router.navigate({ to: '/foo', search: { foo: 'baz' } })
expect(onStay).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ id: '/foo', search: { foo: 'baz' } }),
+ expect.objectContaining({ id: '/foo/foo', search: { foo: 'baz' } }),
)
// Staying on foo
await router.navigate({ to: '/foo', search: { foo: 'quux' } })
expect(onStay).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ id: '/foo', search: { foo: 'quux' } }),
+ expect.objectContaining({ id: '/foo/foo', search: { foo: 'quux' } }),
)
})
})
diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts
index 031405bcfb1..be9c248ac12 100644
--- a/packages/router-core/tests/load.test.ts
+++ b/packages/router-core/tests/load.test.ts
@@ -51,14 +51,14 @@ describe('beforeLoad skip or exec', () => {
const navigation = router.navigate({ to: '/foo' })
expect(beforeLoad).toHaveBeenCalledTimes(1)
expect(router.state.pendingMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await navigation
expect(router.state.location.pathname).toBe('/foo')
expect(router.state.matches).toEqual(
expect.arrayContaining([
expect.objectContaining({
- id: '/foo',
+ id: '/foo/foo',
context: {
hello: 'world',
},
@@ -73,7 +73,7 @@ describe('beforeLoad skip or exec', () => {
const router = setup({ beforeLoad })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await sleep(10)
await router.navigate({ to: '/foo' })
@@ -87,7 +87,7 @@ describe('beforeLoad skip or exec', () => {
router.preloadRoute({ to: '/foo' })
await Promise.resolve()
expect(router.state.cachedMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await router.navigate({ to: '/foo' })
@@ -233,14 +233,14 @@ describe('loader skip or exec', () => {
const navigation = router.navigate({ to: '/foo' })
expect(loader).toHaveBeenCalledTimes(1)
expect(router.state.pendingMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await navigation
expect(router.state.location.pathname).toBe('/foo')
expect(router.state.matches).toEqual(
expect.arrayContaining([
expect.objectContaining({
- id: '/foo',
+ id: '/foo/foo',
loaderData: {
hello: 'world',
},
@@ -255,7 +255,7 @@ describe('loader skip or exec', () => {
const router = setup({ loader })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await sleep(10)
await router.navigate({ to: '/foo' })
@@ -268,7 +268,7 @@ describe('loader skip or exec', () => {
const router = setup({ loader, staleTime: 1000 })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await sleep(10)
await router.navigate({ to: '/foo' })
@@ -282,7 +282,7 @@ describe('loader skip or exec', () => {
router.preloadRoute({ to: '/foo' })
await Promise.resolve()
expect(router.state.cachedMatches).toEqual(
- expect.arrayContaining([expect.objectContaining({ id: '/foo' })]),
+ expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
)
await router.navigate({ to: '/foo' })
@@ -494,7 +494,7 @@ test('cancelMatches after pending timeout', async () => {
await sleep(WAIT_TIME * 30)
// At this point, pending timeout should have triggered
- const fooMatch = router.getMatch('/foo')
+ const fooMatch = router.getMatch('/foo/foo')
expect(fooMatch).toBeDefined()
// Navigate away, which should cancel the pending match
@@ -505,7 +505,7 @@ test('cancelMatches after pending timeout', async () => {
// Verify that abort was called and pending timeout was cleared
expect(onAbortMock).toHaveBeenCalled()
- const cancelledFooMatch = router.getMatch('/foo')
+ const cancelledFooMatch = router.getMatch('/foo/foo')
expect(cancelledFooMatch?._nonReactive.pendingTimeout).toBeUndefined()
})
diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
index 66a4721cb6e..081f5f9073e 100644
--- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
+++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
@@ -178,7 +178,6 @@ function RouteComp({
const interpolated = interpolatePath({
path: route.fullPath,
params: allParams,
- leaveWildcards: false,
leaveParams: false,
decodeCharMap: router().pathParamsDecodeCharMap,
})
diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx
index ddf5acc206f..ae62c408ecf 100644
--- a/packages/solid-router/src/Match.tsx
+++ b/packages/solid-router/src/Match.tsx
@@ -105,7 +105,7 @@ export const Match = (props: { matchId: string }) => {
onCatch={(error: Error) => {
// Forward not found errors (we don't want to show the error component for these)
if (isNotFound(error)) throw error
- warning(false, `Error in route match: ${props.matchId}`)
+ warning(false, `Error in route match: ${matchState()!.routeId}`)
routeOnCatch()?.(error)
}}
>
@@ -411,7 +411,7 @@ export const Outlet = () => {
return (
}
>