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 ( } >