diff --git a/e2e/react-router/basic-file-based/src/components/RenderCounter.tsx b/e2e/react-router/basic-file-based/src/components/RenderCounter.tsx new file mode 100644 index 00000000000..38eff63cd32 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/components/RenderCounter.tsx @@ -0,0 +1,7 @@ +import { useRef } from 'react' + +export const RenderCounter = () => { + const renderCounter = useRef(0) + renderCounter.current = renderCounter.current + 1 + return <>{renderCounter.current} +} diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts index 62af050d6e8..6c4c9159418 100644 --- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts @@ -57,21 +57,23 @@ import { Route as ParamsPsWildcardPrefixChar123Char125RouteImport } from './rout import { Route as ParamsPsWildcardSplatRouteImport } from './routes/params-ps/wildcard/$' import { Route as ParamsPsNamedChar123fooChar125suffixRouteImport } from './routes/params-ps/named/{$foo}suffix' import { Route as ParamsPsNamedPrefixChar123fooChar125RouteImport } from './routes/params-ps/named/prefix{$foo}' -import { Route as ParamsPsNamedFooRouteImport } from './routes/params-ps/named/$foo' import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b' import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a' import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside' import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout' +import { Route as ParamsPsNamedFooRouteRouteImport } from './routes/params-ps/named/$foo/route' import { Route as RelativeUseNavigateWithSearchIndexRouteImport } from './routes/relative/useNavigate/with-search/index' import { Route as RelativeUseNavigatePathIndexRouteImport } from './routes/relative/useNavigate/path/index' import { Route as RelativeUseNavigateNestedIndexRouteImport } from './routes/relative/useNavigate/nested/index' import { Route as RelativeLinkWithSearchIndexRouteImport } from './routes/relative/link/with-search/index' import { Route as RelativeLinkPathIndexRouteImport } from './routes/relative/link/path/index' import { Route as RelativeLinkNestedIndexRouteImport } from './routes/relative/link/nested/index' +import { Route as ParamsPsNamedFooBarRouteRouteImport } from './routes/params-ps/named/$foo/$bar.route' import { Route as RelativeUseNavigatePathPathIndexRouteImport } from './routes/relative/useNavigate/path/$path/index' import { Route as RelativeUseNavigateNestedDeepIndexRouteImport } from './routes/relative/useNavigate/nested/deep/index' import { Route as RelativeLinkPathPathIndexRouteImport } from './routes/relative/link/path/$path/index' import { Route as RelativeLinkNestedDeepIndexRouteImport } from './routes/relative/link/nested/deep/index' +import { Route as ParamsPsNamedFooBarBazRouteImport } from './routes/params-ps/named/$foo/$bar.$baz' const groupRouteImport = createFileRoute('/(group)')() @@ -321,11 +323,6 @@ const ParamsPsNamedPrefixChar123fooChar125Route = path: '/params-ps/named/prefix{$foo}', getParentRoute: () => rootRouteImport, } as any) -const ParamsPsNamedFooRoute = ParamsPsNamedFooRouteImport.update({ - id: '/params-ps/named/$foo', - path: '/params-ps/named/$foo', - getParentRoute: () => rootRouteImport, -} as any) const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({ id: '/layout-b', path: '/layout-b', @@ -346,6 +343,11 @@ const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({ path: '/insidelayout', getParentRoute: () => groupLayoutRoute, } as any) +const ParamsPsNamedFooRouteRoute = ParamsPsNamedFooRouteRouteImport.update({ + id: '/params-ps/named/$foo', + path: '/params-ps/named/$foo', + getParentRoute: () => rootRouteImport, +} as any) const RelativeUseNavigateWithSearchIndexRoute = RelativeUseNavigateWithSearchIndexRouteImport.update({ id: '/with-search/', @@ -380,6 +382,12 @@ const RelativeLinkNestedIndexRoute = RelativeLinkNestedIndexRouteImport.update({ path: '/nested/', getParentRoute: () => RelativeLinkRouteRoute, } as any) +const ParamsPsNamedFooBarRouteRoute = + ParamsPsNamedFooBarRouteRouteImport.update({ + id: '/$bar', + path: '/$bar', + getParentRoute: () => ParamsPsNamedFooRouteRoute, + } as any) const RelativeUseNavigatePathPathIndexRoute = RelativeUseNavigatePathPathIndexRouteImport.update({ id: '/path/$path/', @@ -404,6 +412,11 @@ const RelativeLinkNestedDeepIndexRoute = path: '/nested/deep/', getParentRoute: () => RelativeLinkRouteRoute, } as any) +const ParamsPsNamedFooBarBazRoute = ParamsPsNamedFooBarBazRouteImport.update({ + id: '/$baz', + path: '/$baz', + getParentRoute: () => ParamsPsNamedFooBarRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof groupLayoutRouteWithChildren @@ -430,11 +443,11 @@ export interface FileRoutesByFullPath { '/redirect': typeof RedirectIndexRoute '/relative': typeof RelativeIndexRoute '/search-params/': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute - '/params-ps/named/$foo': typeof ParamsPsNamedFooRoute '/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route '/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute '/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute @@ -454,12 +467,14 @@ export interface FileRoutesByFullPath { '/params-ps/named': typeof ParamsPsNamedIndexRoute '/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute '/redirect/$target/': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested': typeof RelativeLinkNestedIndexRoute '/relative/link/path': typeof RelativeLinkPathIndexRoute '/relative/link/with-search': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -487,11 +502,11 @@ export interface FileRoutesByTo { '/redirect': typeof RedirectIndexRoute '/relative': typeof RelativeIndexRoute '/search-params': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute - '/params-ps/named/$foo': typeof ParamsPsNamedFooRoute '/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route '/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute '/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute @@ -511,12 +526,14 @@ export interface FileRoutesByTo { '/params-ps/named': typeof ParamsPsNamedIndexRoute '/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute '/redirect/$target': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested': typeof RelativeLinkNestedIndexRoute '/relative/link/path': typeof RelativeLinkPathIndexRoute '/relative/link/with-search': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -552,11 +569,11 @@ export interface FileRoutesById { '/redirect/': typeof RedirectIndexRoute '/relative/': typeof RelativeIndexRoute '/search-params/': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute - '/params-ps/named/$foo': typeof ParamsPsNamedFooRoute '/params-ps/named/prefix{$foo}': typeof ParamsPsNamedPrefixChar123fooChar125Route '/params-ps/named/{$foo}suffix': typeof ParamsPsNamedChar123fooChar125suffixRoute '/params-ps/wildcard/$': typeof ParamsPsWildcardSplatRoute @@ -576,12 +593,14 @@ export interface FileRoutesById { '/params-ps/named/': typeof ParamsPsNamedIndexRoute '/params-ps/wildcard/': typeof ParamsPsWildcardIndexRoute '/redirect/$target/': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested/': typeof RelativeLinkNestedIndexRoute '/relative/link/path/': typeof RelativeLinkPathIndexRoute '/relative/link/with-search/': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested/': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path/': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search/': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep/': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path/': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep/': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -614,11 +633,11 @@ export interface FileRouteTypes { | '/redirect' | '/relative' | '/search-params/' + | '/params-ps/named/$foo' | '/insidelayout' | '/subfolder/inside' | '/layout-a' | '/layout-b' - | '/params-ps/named/$foo' | '/params-ps/named/prefix{$foo}' | '/params-ps/named/{$foo}suffix' | '/params-ps/wildcard/$' @@ -638,12 +657,14 @@ export interface FileRouteTypes { | '/params-ps/named' | '/params-ps/wildcard' | '/redirect/$target/' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested' | '/relative/link/path' | '/relative/link/with-search' | '/relative/useNavigate/nested' | '/relative/useNavigate/path' | '/relative/useNavigate/with-search' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep' | '/relative/link/path/$path' | '/relative/useNavigate/nested/deep' @@ -671,11 +692,11 @@ export interface FileRouteTypes { | '/redirect' | '/relative' | '/search-params' + | '/params-ps/named/$foo' | '/insidelayout' | '/subfolder/inside' | '/layout-a' | '/layout-b' - | '/params-ps/named/$foo' | '/params-ps/named/prefix{$foo}' | '/params-ps/named/{$foo}suffix' | '/params-ps/wildcard/$' @@ -695,12 +716,14 @@ export interface FileRouteTypes { | '/params-ps/named' | '/params-ps/wildcard' | '/redirect/$target' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested' | '/relative/link/path' | '/relative/link/with-search' | '/relative/useNavigate/nested' | '/relative/useNavigate/path' | '/relative/useNavigate/with-search' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep' | '/relative/link/path/$path' | '/relative/useNavigate/nested/deep' @@ -735,11 +758,11 @@ export interface FileRouteTypes { | '/redirect/' | '/relative/' | '/search-params/' + | '/params-ps/named/$foo' | '/(group)/_layout/insidelayout' | '/(group)/subfolder/inside' | '/_layout/_layout-2/layout-a' | '/_layout/_layout-2/layout-b' - | '/params-ps/named/$foo' | '/params-ps/named/prefix{$foo}' | '/params-ps/named/{$foo}suffix' | '/params-ps/wildcard/$' @@ -759,12 +782,14 @@ export interface FileRouteTypes { | '/params-ps/named/' | '/params-ps/wildcard/' | '/redirect/$target/' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested/' | '/relative/link/path/' | '/relative/link/with-search/' | '/relative/useNavigate/nested/' | '/relative/useNavigate/path/' | '/relative/useNavigate/with-search/' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep/' | '/relative/link/path/$path/' | '/relative/useNavigate/nested/deep/' @@ -792,7 +817,7 @@ export interface RootRouteChildren { ParamsPsIndexRoute: typeof ParamsPsIndexRoute RedirectIndexRoute: typeof RedirectIndexRoute RelativeIndexRoute: typeof RelativeIndexRoute - ParamsPsNamedFooRoute: typeof ParamsPsNamedFooRoute + ParamsPsNamedFooRouteRoute: typeof ParamsPsNamedFooRouteRouteWithChildren ParamsPsNamedPrefixChar123fooChar125Route: typeof ParamsPsNamedPrefixChar123fooChar125Route ParamsPsNamedChar123fooChar125suffixRoute: typeof ParamsPsNamedChar123fooChar125suffixRoute ParamsPsWildcardSplatRoute: typeof ParamsPsWildcardSplatRoute @@ -1138,13 +1163,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ParamsPsNamedPrefixChar123fooChar125RouteImport parentRoute: typeof rootRouteImport } - '/params-ps/named/$foo': { - id: '/params-ps/named/$foo' - path: '/params-ps/named/$foo' - fullPath: '/params-ps/named/$foo' - preLoaderRoute: typeof ParamsPsNamedFooRouteImport - parentRoute: typeof rootRouteImport - } '/_layout/_layout-2/layout-b': { id: '/_layout/_layout-2/layout-b' path: '/layout-b' @@ -1173,6 +1191,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport parentRoute: typeof groupLayoutRoute } + '/params-ps/named/$foo': { + id: '/params-ps/named/$foo' + path: '/params-ps/named/$foo' + fullPath: '/params-ps/named/$foo' + preLoaderRoute: typeof ParamsPsNamedFooRouteRouteImport + parentRoute: typeof rootRouteImport + } '/relative/useNavigate/with-search/': { id: '/relative/useNavigate/with-search/' path: '/with-search' @@ -1215,6 +1240,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RelativeLinkNestedIndexRouteImport parentRoute: typeof RelativeLinkRouteRoute } + '/params-ps/named/$foo/$bar': { + id: '/params-ps/named/$foo/$bar' + path: '/$bar' + fullPath: '/params-ps/named/$foo/$bar' + preLoaderRoute: typeof ParamsPsNamedFooBarRouteRouteImport + parentRoute: typeof ParamsPsNamedFooRouteRoute + } '/relative/useNavigate/path/$path/': { id: '/relative/useNavigate/path/$path/' path: '/path/$path' @@ -1243,6 +1275,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RelativeLinkNestedDeepIndexRouteImport parentRoute: typeof RelativeLinkRouteRoute } + '/params-ps/named/$foo/$bar/$baz': { + id: '/params-ps/named/$foo/$bar/$baz' + path: '/$baz' + fullPath: '/params-ps/named/$foo/$bar/$baz' + preLoaderRoute: typeof ParamsPsNamedFooBarBazRouteImport + parentRoute: typeof ParamsPsNamedFooBarRouteRoute + } } } @@ -1394,6 +1433,33 @@ const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( RedirectTargetRouteChildren, ) +interface ParamsPsNamedFooBarRouteRouteChildren { + ParamsPsNamedFooBarBazRoute: typeof ParamsPsNamedFooBarBazRoute +} + +const ParamsPsNamedFooBarRouteRouteChildren: ParamsPsNamedFooBarRouteRouteChildren = + { + ParamsPsNamedFooBarBazRoute: ParamsPsNamedFooBarBazRoute, + } + +const ParamsPsNamedFooBarRouteRouteWithChildren = + ParamsPsNamedFooBarRouteRoute._addFileChildren( + ParamsPsNamedFooBarRouteRouteChildren, + ) + +interface ParamsPsNamedFooRouteRouteChildren { + ParamsPsNamedFooBarRouteRoute: typeof ParamsPsNamedFooBarRouteRouteWithChildren +} + +const ParamsPsNamedFooRouteRouteChildren: ParamsPsNamedFooRouteRouteChildren = { + ParamsPsNamedFooBarRouteRoute: ParamsPsNamedFooBarRouteRouteWithChildren, +} + +const ParamsPsNamedFooRouteRouteWithChildren = + ParamsPsNamedFooRouteRoute._addFileChildren( + ParamsPsNamedFooRouteRouteChildren, + ) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren, @@ -1416,7 +1482,7 @@ const rootRouteChildren: RootRouteChildren = { ParamsPsIndexRoute: ParamsPsIndexRoute, RedirectIndexRoute: RedirectIndexRoute, RelativeIndexRoute: RelativeIndexRoute, - ParamsPsNamedFooRoute: ParamsPsNamedFooRoute, + ParamsPsNamedFooRouteRoute: ParamsPsNamedFooRouteRouteWithChildren, ParamsPsNamedPrefixChar123fooChar125Route: ParamsPsNamedPrefixChar123fooChar125Route, ParamsPsNamedChar123fooChar125suffixRoute: diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo.tsx deleted file mode 100644 index dae4251e293..00000000000 --- a/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -export const Route = createFileRoute('/params-ps/named/$foo')({ - component: RouteComponent, -}) - -function RouteComponent() { - const p = Route.useParams() - return ( -
-

ParamsNamedFoo

-
{JSON.stringify(p)}
-
- ) -} diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx new file mode 100644 index 00000000000..2a38545def6 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/params-ps/named/$foo/$bar/$baz')({ + component: RouteComponent, +}) + +function RouteComponent() { + const { foo, bar, baz } = Route.useParams() + return ( +
+ Hello "/params-ps/named/{foo}/{bar}/{baz}"! +
+ baz: {baz} +
+
+ ) +} diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx new file mode 100644 index 00000000000..653dcd951a2 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx @@ -0,0 +1,45 @@ +import { + Link, + Outlet, + createFileRoute, + useParams, +} from '@tanstack/react-router' +import { RenderCounter } from '../../../../components/RenderCounter' + +export const Route = createFileRoute('/params-ps/named/$foo/$bar')({ + component: RouteComponent, +}) + +function RouteComponent() { + const { foo, bar, baz } = useParams({ + strict: false, + }) + + return ( +
+ Hello "/params-ps/named/{foo}/{bar}"! +
+ Bar Render Count:{' '} + + + +
+
+ Bar: {bar} +
+
+ Baz in Bar:{' '} + {baz ?? 'no param'} +
+ + To Baz + + +
+ ) +} diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx new file mode 100644 index 00000000000..432facc3cc7 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx @@ -0,0 +1,42 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { RenderCounter } from '../../../../components/RenderCounter' + +export const Route = createFileRoute('/params-ps/named/$foo')({ + component: RouteComponent, +}) + +function RouteComponent() { + const foo = Route.useParams() + return ( +
+

ParamsNamedFoo

+
+ RenderCount:{' '} + + + +
+
{JSON.stringify(foo)}
+ + Index + + + Bar1 + + + Bar2 + + +
+ ) +} diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/named/index.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/named/index.tsx index f9a9a7ac06e..e11cbd530cd 100644 --- a/e2e/react-router/basic-file-based/src/routes/params-ps/named/index.tsx +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/named/index.tsx @@ -1,5 +1,4 @@ -import { createFileRoute } from '@tanstack/react-router' -import { redirect } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/params-ps/named/')({ beforeLoad: () => { 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 546fa80d31e..59dc844fd81 100644 --- a/e2e/react-router/basic-file-based/tests/params.spec.ts +++ b/e2e/react-router/basic-file-based/tests/params.spec.ts @@ -129,6 +129,80 @@ test.describe('params operations + prefix/suffix', () => { expect(paramsObj).toEqual(params) }) }) + + test(`ensure use params doesn't cause excess renders and is stable across various usage options`, async ({ + page, + }) => { + await page.goto('/params-ps/named/foo') + await page.waitForLoadState('networkidle') + + const pagePathname = new URL(page.url()).pathname + expect(pagePathname).toBe('/params-ps/named/foo') + + const fooRenderCount = page.getByTestId('foo-render-count') + const fooIndexLink = page.getByTestId('params-foo-links-index') + const fooBar1Link = page.getByTestId('params-foo-links-bar1') + const fooBar2Link = page.getByTestId('params-foo-links-bar2') + const fooBarBazLink = page.getByTestId('params-foo-bar-links-baz') + const fooValue = page.getByTestId('params-output') + const fooBarValue = page.getByTestId('foo-bar-value') + const fooBazInBarValue = page.getByTestId('foo-baz-in-bar-value') + const fooBarRenderCount = page.getByTestId('foo-bar-render-count') + const fooBarBazValue = page.getByTestId('foo-bar-baz-value') + + await expect(fooRenderCount).toBeInViewport() + await expect(fooValue).toBeInViewport() + await expect(fooIndexLink).toBeInViewport() + await expect(fooBar1Link).toBeInViewport() + await expect(fooBar2Link).toBeInViewport() + await expect(fooRenderCount).toHaveText('1') + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + + await fooBar1Link.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooBazInBarValue).toBeInViewport() + await expect(fooBarBazLink).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarRenderCount).toHaveText('1') + await expect(fooBarValue).toHaveText('1') + await expect(fooBazInBarValue).toHaveText('no param') + + await fooBarBazLink.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooBazInBarValue).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarRenderCount).toHaveText('2') + await expect(fooBarValue).toHaveText('1') + await expect(fooBazInBarValue).toHaveText('1_10') + await expect(fooBarBazValue).toHaveText('1_10') + + await fooBar2Link.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarValue).toHaveText('2') + + await fooIndexLink.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarValue).not.toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + }) }) test.describe('wildcard param', () => { diff --git a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts index b81fbe69d5d..1fa808525cb 100644 --- a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts @@ -51,16 +51,19 @@ import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layo import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a' import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside' import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout' +import { Route as ParamsPsNamedFooRouteRouteImport } from './routes/params-ps/named/$foo/route' import { Route as RelativeUseNavigateWithSearchIndexRouteImport } from './routes/relative/useNavigate/with-search/index' import { Route as RelativeUseNavigatePathIndexRouteImport } from './routes/relative/useNavigate/path/index' import { Route as RelativeUseNavigateNestedIndexRouteImport } from './routes/relative/useNavigate/nested/index' import { Route as RelativeLinkWithSearchIndexRouteImport } from './routes/relative/link/with-search/index' import { Route as RelativeLinkPathIndexRouteImport } from './routes/relative/link/path/index' import { Route as RelativeLinkNestedIndexRouteImport } from './routes/relative/link/nested/index' +import { Route as ParamsPsNamedFooBarRouteRouteImport } from './routes/params-ps/named/$foo/$bar.route' import { Route as RelativeUseNavigatePathPathIndexRouteImport } from './routes/relative/useNavigate/path/$path/index' import { Route as RelativeUseNavigateNestedDeepIndexRouteImport } from './routes/relative/useNavigate/nested/deep/index' import { Route as RelativeLinkPathPathIndexRouteImport } from './routes/relative/link/path/$path/index' import { Route as RelativeLinkNestedDeepIndexRouteImport } from './routes/relative/link/nested/deep/index' +import { Route as ParamsPsNamedFooBarBazRouteImport } from './routes/params-ps/named/$foo/$bar.$baz' const groupRouteImport = createFileRoute('/(group)')() @@ -274,6 +277,11 @@ const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({ path: '/insidelayout', getParentRoute: () => groupLayoutRoute, } as any) +const ParamsPsNamedFooRouteRoute = ParamsPsNamedFooRouteRouteImport.update({ + id: '/params-ps/named/$foo', + path: '/params-ps/named/$foo', + getParentRoute: () => rootRouteImport, +} as any) const RelativeUseNavigateWithSearchIndexRoute = RelativeUseNavigateWithSearchIndexRouteImport.update({ id: '/with-search/', @@ -308,6 +316,12 @@ const RelativeLinkNestedIndexRoute = RelativeLinkNestedIndexRouteImport.update({ path: '/nested/', getParentRoute: () => RelativeLinkRouteRoute, } as any) +const ParamsPsNamedFooBarRouteRoute = + ParamsPsNamedFooBarRouteRouteImport.update({ + id: '/$bar', + path: '/$bar', + getParentRoute: () => ParamsPsNamedFooRouteRoute, + } as any) const RelativeUseNavigatePathPathIndexRoute = RelativeUseNavigatePathPathIndexRouteImport.update({ id: '/path/$path/', @@ -332,6 +346,11 @@ const RelativeLinkNestedDeepIndexRoute = path: '/nested/deep/', getParentRoute: () => RelativeLinkRouteRoute, } as any) +const ParamsPsNamedFooBarBazRoute = ParamsPsNamedFooBarBazRouteImport.update({ + id: '/$baz', + path: '/$baz', + getParentRoute: () => ParamsPsNamedFooBarRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof groupLayoutRouteWithChildren @@ -355,6 +374,7 @@ export interface FileRoutesByFullPath { '/redirect': typeof RedirectIndexRoute '/relative': typeof RelativeIndexRoute '/search-params/': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute @@ -371,12 +391,14 @@ export interface FileRoutesByFullPath { '/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute '/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute '/redirect/$target/': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested': typeof RelativeLinkNestedIndexRoute '/relative/link/path': typeof RelativeLinkPathIndexRoute '/relative/link/with-search': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -401,6 +423,7 @@ export interface FileRoutesByTo { '/redirect': typeof RedirectIndexRoute '/relative': typeof RelativeIndexRoute '/search-params': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute @@ -417,12 +440,14 @@ export interface FileRoutesByTo { '/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute '/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute '/redirect/$target': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested': typeof RelativeLinkNestedIndexRoute '/relative/link/path': typeof RelativeLinkPathIndexRoute '/relative/link/with-search': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -455,6 +480,7 @@ export interface FileRoutesById { '/redirect/': typeof RedirectIndexRoute '/relative/': typeof RelativeIndexRoute '/search-params/': typeof SearchParamsIndexRoute + '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute @@ -471,12 +497,14 @@ export interface FileRoutesById { '/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute '/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute '/redirect/$target/': typeof RedirectTargetIndexRoute + '/params-ps/named/$foo/$bar': typeof ParamsPsNamedFooBarRouteRouteWithChildren '/relative/link/nested/': typeof RelativeLinkNestedIndexRoute '/relative/link/path/': typeof RelativeLinkPathIndexRoute '/relative/link/with-search/': typeof RelativeLinkWithSearchIndexRoute '/relative/useNavigate/nested/': typeof RelativeUseNavigateNestedIndexRoute '/relative/useNavigate/path/': typeof RelativeUseNavigatePathIndexRoute '/relative/useNavigate/with-search/': typeof RelativeUseNavigateWithSearchIndexRoute + '/params-ps/named/$foo/$bar/$baz': typeof ParamsPsNamedFooBarBazRoute '/relative/link/nested/deep/': typeof RelativeLinkNestedDeepIndexRoute '/relative/link/path/$path/': typeof RelativeLinkPathPathIndexRoute '/relative/useNavigate/nested/deep/': typeof RelativeUseNavigateNestedDeepIndexRoute @@ -506,6 +534,7 @@ export interface FileRouteTypes { | '/redirect' | '/relative' | '/search-params/' + | '/params-ps/named/$foo' | '/insidelayout' | '/subfolder/inside' | '/layout-a' @@ -522,12 +551,14 @@ export interface FileRouteTypes { | '/relative/useNavigate/relative-useNavigate-a' | '/relative/useNavigate/relative-useNavigate-b' | '/redirect/$target/' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested' | '/relative/link/path' | '/relative/link/with-search' | '/relative/useNavigate/nested' | '/relative/useNavigate/path' | '/relative/useNavigate/with-search' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep' | '/relative/link/path/$path' | '/relative/useNavigate/nested/deep' @@ -552,6 +583,7 @@ export interface FileRouteTypes { | '/redirect' | '/relative' | '/search-params' + | '/params-ps/named/$foo' | '/insidelayout' | '/subfolder/inside' | '/layout-a' @@ -568,12 +600,14 @@ export interface FileRouteTypes { | '/relative/useNavigate/relative-useNavigate-a' | '/relative/useNavigate/relative-useNavigate-b' | '/redirect/$target' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested' | '/relative/link/path' | '/relative/link/with-search' | '/relative/useNavigate/nested' | '/relative/useNavigate/path' | '/relative/useNavigate/with-search' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep' | '/relative/link/path/$path' | '/relative/useNavigate/nested/deep' @@ -605,6 +639,7 @@ export interface FileRouteTypes { | '/redirect/' | '/relative/' | '/search-params/' + | '/params-ps/named/$foo' | '/(group)/_layout/insidelayout' | '/(group)/subfolder/inside' | '/_layout/_layout-2/layout-a' @@ -621,12 +656,14 @@ export interface FileRouteTypes { | '/relative/useNavigate/relative-useNavigate-a' | '/relative/useNavigate/relative-useNavigate-b' | '/redirect/$target/' + | '/params-ps/named/$foo/$bar' | '/relative/link/nested/' | '/relative/link/path/' | '/relative/link/with-search/' | '/relative/useNavigate/nested/' | '/relative/useNavigate/path/' | '/relative/useNavigate/with-search/' + | '/params-ps/named/$foo/$bar/$baz' | '/relative/link/nested/deep/' | '/relative/link/path/$path/' | '/relative/useNavigate/nested/deep/' @@ -651,6 +688,7 @@ export interface RootRouteChildren { RedirectTargetRoute: typeof RedirectTargetRouteWithChildren RedirectIndexRoute: typeof RedirectIndexRoute RelativeIndexRoute: typeof RelativeIndexRoute + ParamsPsNamedFooRouteRoute: typeof ParamsPsNamedFooRouteRouteWithChildren ParamsSingleValueRoute: typeof ParamsSingleValueRoute PostsPostIdEditRoute: typeof PostsPostIdEditRoute RedirectPreloadFirstRoute: typeof RedirectPreloadFirstRoute @@ -947,6 +985,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport parentRoute: typeof groupLayoutRoute } + '/params-ps/named/$foo': { + id: '/params-ps/named/$foo' + path: '/params-ps/named/$foo' + fullPath: '/params-ps/named/$foo' + preLoaderRoute: typeof ParamsPsNamedFooRouteRouteImport + parentRoute: typeof rootRouteImport + } '/relative/useNavigate/with-search/': { id: '/relative/useNavigate/with-search/' path: '/with-search' @@ -989,6 +1034,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof RelativeLinkNestedIndexRouteImport parentRoute: typeof RelativeLinkRouteRoute } + '/params-ps/named/$foo/$bar': { + id: '/params-ps/named/$foo/$bar' + path: '/$bar' + fullPath: '/params-ps/named/$foo/$bar' + preLoaderRoute: typeof ParamsPsNamedFooBarRouteRouteImport + parentRoute: typeof ParamsPsNamedFooRouteRoute + } '/relative/useNavigate/path/$path/': { id: '/relative/useNavigate/path/$path/' path: '/path/$path' @@ -1017,6 +1069,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof RelativeLinkNestedDeepIndexRouteImport parentRoute: typeof RelativeLinkRouteRoute } + '/params-ps/named/$foo/$bar/$baz': { + id: '/params-ps/named/$foo/$bar/$baz' + path: '/$baz' + fullPath: '/params-ps/named/$foo/$bar/$baz' + preLoaderRoute: typeof ParamsPsNamedFooBarBazRouteImport + parentRoute: typeof ParamsPsNamedFooBarRouteRoute + } } } @@ -1168,6 +1227,33 @@ const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( RedirectTargetRouteChildren, ) +interface ParamsPsNamedFooBarRouteRouteChildren { + ParamsPsNamedFooBarBazRoute: typeof ParamsPsNamedFooBarBazRoute +} + +const ParamsPsNamedFooBarRouteRouteChildren: ParamsPsNamedFooBarRouteRouteChildren = + { + ParamsPsNamedFooBarBazRoute: ParamsPsNamedFooBarBazRoute, + } + +const ParamsPsNamedFooBarRouteRouteWithChildren = + ParamsPsNamedFooBarRouteRoute._addFileChildren( + ParamsPsNamedFooBarRouteRouteChildren, + ) + +interface ParamsPsNamedFooRouteRouteChildren { + ParamsPsNamedFooBarRouteRoute: typeof ParamsPsNamedFooBarRouteRouteWithChildren +} + +const ParamsPsNamedFooRouteRouteChildren: ParamsPsNamedFooRouteRouteChildren = { + ParamsPsNamedFooBarRouteRoute: ParamsPsNamedFooBarRouteRouteWithChildren, +} + +const ParamsPsNamedFooRouteRouteWithChildren = + ParamsPsNamedFooRouteRoute._addFileChildren( + ParamsPsNamedFooRouteRouteChildren, + ) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren, @@ -1186,6 +1272,7 @@ const rootRouteChildren: RootRouteChildren = { RedirectTargetRoute: RedirectTargetRouteWithChildren, RedirectIndexRoute: RedirectIndexRoute, RelativeIndexRoute: RelativeIndexRoute, + ParamsPsNamedFooRouteRoute: ParamsPsNamedFooRouteRouteWithChildren, ParamsSingleValueRoute: ParamsSingleValueRoute, PostsPostIdEditRoute: PostsPostIdEditRoute, RedirectPreloadFirstRoute: RedirectPreloadFirstRoute, diff --git a/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx new file mode 100644 index 00000000000..0b6488f389e --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.$baz.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/params-ps/named/$foo/$bar/$baz')({ + component: RouteComponent, +}) + +function RouteComponent() { + const params = Route.useParams() + return ( +
+ Hello "/params-ps/named/$foo/$bar/$baz"! +
+ baz: {params().baz} +
+
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx new file mode 100644 index 00000000000..4387aed56d5 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/$bar.route.tsx @@ -0,0 +1,64 @@ +import { + Link, + Outlet, + createFileRoute, + useParams, +} from '@tanstack/solid-router' +import { createEffect, createSignal } from 'solid-js' + +export const Route = createFileRoute('/params-ps/named/$foo/$bar')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [renderBarCount, setBarRenderCount] = createSignal(0) + const [renderBazCount, setBazRenderCount] = createSignal(0) + + const params = useParams({ + strict: false, + }) + + createEffect(() => { + params().bar + setBarRenderCount((prev) => prev + 1) + }) + + createEffect(() => { + params().baz + setBazRenderCount((prev) => prev + 1) + }) + + return ( +
+ Hello "/params-ps/named/$foo/$bar"! +
+ Bar Render Count:{' '} + {renderBarCount()} +
+
+ Bar: {params().bar} +
+
+ Baz in Bar Render Count:{' '} + + {renderBazCount()} + +
+
+ Baz in Bar:{' '} + + {params().baz ?? 'no param'} + +
+ + To Baz + + +
+ ) +} diff --git a/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx new file mode 100644 index 00000000000..58a78e38223 --- /dev/null +++ b/e2e/solid-router/basic-file-based/src/routes/params-ps/named/$foo/route.tsx @@ -0,0 +1,47 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { createEffect, createSignal } from 'solid-js' + +export const Route = createFileRoute('/params-ps/named/$foo')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [renderCount, setRenderCount] = createSignal(0) + const params = Route.useParams() + + createEffect(() => { + params().foo + + setRenderCount((prev) => prev + 1) + }) + + return ( +
+

ParamsNamedFoo

+
+ RenderCount: {renderCount()} +
+
{JSON.stringify(params())}
+ + Index + + + Bar1 + + + Bar2 + + +
+ ) +} 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 e95bc56c598..42930fc41a1 100644 --- a/e2e/solid-router/basic-file-based/tests/params.spec.ts +++ b/e2e/solid-router/basic-file-based/tests/params.spec.ts @@ -54,3 +54,78 @@ test.describe('ensure single params have been parsed correctly whilst being stab }) } }) + +test('ensure only applicable params are returned and updated with multiple params', async ({ + page, +}) => { + await page.goto('/params-ps/named/foo') + await page.waitForLoadState('networkidle') + + const pagePathname = new URL(page.url()).pathname + expect(pagePathname).toBe('/params-ps/named/foo') + + const fooRenderCount = page.getByTestId('foo-render-count') + const fooIndexLink = page.getByTestId('params-foo-links-index') + const fooBar1Link = page.getByTestId('params-foo-links-bar1') + const fooBar2Link = page.getByTestId('params-foo-links-bar2') + const fooBarBazLink = page.getByTestId('params-foo-bar-links-baz') + const fooValue = page.getByTestId('params-output') + const fooBarValue = page.getByTestId('foo-bar-value') + const fooBazInBarValue = page.getByTestId('foo-baz-in-bar-value') + const fooBarRenderCount = page.getByTestId('foo-bar-render-count') + const fooBazInBarRenderCount = page.getByTestId('foo-baz-in-bar-render-count') + const fooBarBazValue = page.getByTestId('foo-bar-baz-value') + + await expect(fooRenderCount).toBeInViewport() + await expect(fooValue).toBeInViewport() + await expect(fooIndexLink).toBeInViewport() + await expect(fooBar1Link).toBeInViewport() + await expect(fooBar2Link).toBeInViewport() + await expect(fooRenderCount).toHaveText('1') + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + + await fooBar1Link.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooBazInBarValue).toBeInViewport() + await expect(fooBarBazLink).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarRenderCount).toHaveText('1') + await expect(fooBarValue).toHaveText('1') + await expect(fooBazInBarValue).toHaveText('no param') + await expect(fooBazInBarRenderCount).toHaveText('1') + + await fooBarBazLink.click() + await page.waitForLoadState('networkidle') + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooBazInBarValue).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarRenderCount).toHaveText('1') + await expect(fooBarValue).toHaveText('1') + await expect(fooBazInBarValue).toHaveText('1_10') + await expect(fooBarBazValue).toHaveText('1_10') + await expect(fooBazInBarRenderCount).toHaveText('2') + + await fooBar2Link.click() + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarValue).toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') + await expect(fooBarValue).toHaveText('2') + + await fooIndexLink.click() + await expect(fooValue).toBeInViewport() + await expect(fooRenderCount).toBeInViewport() + await expect(fooBarValue).not.toBeInViewport() + await expect(fooValue).toHaveText(JSON.stringify({ foo: 'foo' })) + await expect(fooRenderCount).toHaveText('1') +}) diff --git a/packages/react-router/src/useParams.tsx b/packages/react-router/src/useParams.tsx index 59fbd5bfbb0..7ff3d3934db 100644 --- a/packages/react-router/src/useParams.tsx +++ b/packages/react-router/src/useParams.tsx @@ -83,11 +83,13 @@ export function useParams< > { return useMatch({ from: opts.from!, - strict: opts.strict, shouldThrow: opts.shouldThrow, structuralSharing: opts.structuralSharing, - select: (match: any) => { - return opts.select ? opts.select(match.params) : match.params + strict: opts.strict, + select: (match) => { + const params = opts.strict === false ? match.params : match._strictParams + + return opts.select ? opts.select(params) : params }, }) as any } diff --git a/packages/react-router/tests/optional-path-params.test.tsx b/packages/react-router/tests/optional-path-params.test.tsx index 23dcf5ab626..e5df289c825 100644 --- a/packages/react-router/tests/optional-path-params.test.tsx +++ b/packages/react-router/tests/optional-path-params.test.tsx @@ -781,7 +781,7 @@ describe('React Router - Optional Path Parameters', () => { getParentRoute: () => postsRoute, path: '/{-$slug}', component: () => { - const { slug } = postsRoute.useParams() + const { slug } = postRoute.useParams() return (

Post Detail

diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx index 594a271eaa4..a22dcc22e58 100644 --- a/packages/react-router/tests/router.test.tsx +++ b/packages/react-router/tests/router.test.tsx @@ -1,4 +1,4 @@ -import { act, useEffect } from 'react' +import { act, useEffect, useRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, @@ -885,12 +885,16 @@ describe('router rendering stability', () => { remountDeps: { default?: RemountDepsFn fooId?: RemountDepsFn + foo2Id?: RemountDepsFn barId?: RemountDepsFn + bar2Id?: RemountDepsFn } }) { const mountMocks = { fooId: vi.fn(), + foo2Id: vi.fn(), barId: vi.fn(), + bar2Id: vi.fn(), } const rootRoute = createRootRoute({ @@ -927,12 +931,34 @@ describe('router rendering stability', () => { > Foo3-Bar2 + + Foo2-1-Bar2_1 + + + Foo2-1-Bar2_2 + + + Foo2-2-Bar2_1 +
) }, }) + const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', @@ -940,6 +966,7 @@ describe('router rendering stability', () => { return '' }, }) + const fooIdRoute = createRoute({ getParentRoute: () => rootRoute, path: '/foo/$fooId', @@ -968,7 +995,7 @@ describe('router rendering stability', () => { }) function BarIdRouteComponent() { - const barId = fooIdRoute.useParams({ select: (s) => s.barId }) + const barId = barIdRoute.useParams({ select: (s) => s.barId }) useEffect(() => { mountMocks.barId() @@ -981,8 +1008,57 @@ describe('router rendering stability', () => { ) } + const foo2IdRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/foo2/$foo2Id', + component: Foo2IdRouteComponent, + remountDeps: opts?.remountDeps.foo2Id, + }) + + function Foo2IdRouteComponent() { + const renderCounter = useRef(0) + renderCounter.current = renderCounter.current + 1 + + const { foo2Id } = foo2IdRoute.useParams() + + useEffect(() => { + mountMocks.foo2Id() + }, [foo2Id]) + + return ( +
+ RenderCount:{' '} + {renderCounter.current} + Foo page {foo2Id} + +
+ ) + } + + const bar2IdRoute = createRoute({ + getParentRoute: () => foo2IdRoute, + path: '/bar2/$bar2Id', + component: Bar2IdRouteComponent, + remountDeps: opts?.remountDeps.bar2Id, + }) + + function Bar2IdRouteComponent() { + const { bar2Id } = bar2IdRoute.useParams() + + useEffect(() => { + mountMocks.bar2Id() + }, [bar2Id]) + + return ( +
+ Bar2 page {bar2Id} +
+ ) + } + const routeTree = rootRoute.addChildren([ fooIdRoute.addChildren([barIdRoute]), + foo2IdRoute.addChildren([bar2IdRoute]), indexRoute, ]) const router = createRouter({ @@ -998,19 +1074,37 @@ describe('router rendering stability', () => { const foo3bar1 = await screen.findByTestId('link-foo-3-bar-1') const foo3bar2 = await screen.findByTestId('link-foo-3-bar-2') + const foo_2_1_bar2_1 = await screen.findByTestId('link-foo2-1-bar2-1') + const foo_2_1_bar2_2 = await screen.findByTestId('link-foo2-1-bar2-2') + const foo_2_2_bar2_1 = await screen.findByTestId('link-foo2-2-bar2-1') expect(foo1).toBeInTheDocument() expect(foo2).toBeInTheDocument() expect(foo3bar1).toBeInTheDocument() expect(foo3bar2).toBeInTheDocument() - - return { router, mountMocks, links: { foo1, foo2, foo3bar1, foo3bar2 } } + expect(foo_2_1_bar2_1).toBeInTheDocument() + expect(foo_2_1_bar2_2).toBeInTheDocument() + expect(foo_2_2_bar2_1).toBeInTheDocument() + + return { + router, + mountMocks, + links: { + foo1, + foo2, + foo3bar1, + foo3bar2, + foo_2_1_bar2_1, + foo_2_1_bar2_2, + foo_2_2_bar2_1, + }, + } } async function check( - page: 'fooId' | 'barId', + page: 'fooId' | 'foo2Id' | 'barId' | 'bar2Id', expected: { value: string; mountCount: number }, - mountMocks: Record<'fooId' | 'barId', () => void>, + mountMocks: Record<'fooId' | 'foo2Id' | 'barId' | 'bar2Id', () => void>, ) { const p = await screen.findByTestId(`${page}-page`) expect(p).toBeInTheDocument() @@ -1027,7 +1121,6 @@ describe('router rendering stability', () => { await act(() => fireEvent.click(links.foo1)) await check('fooId', { value: '1', mountCount: 1 }, mountMocks) expect(mountMocks.barId).not.toHaveBeenCalled() - await act(() => fireEvent.click(links.foo2)) await check('fooId', { value: '2', mountCount: 1 }, mountMocks) expect(mountMocks.barId).not.toHaveBeenCalled() @@ -1035,9 +1128,35 @@ describe('router rendering stability', () => { await act(() => fireEvent.click(links.foo3bar1)) await check('fooId', { value: '3', mountCount: 1 }, mountMocks) await check('barId', { value: '1', mountCount: 1 }, mountMocks) + await act(() => fireEvent.click(links.foo3bar2)) await check('fooId', { value: '3', mountCount: 1 }, mountMocks) await check('barId', { value: '2', mountCount: 1 }, mountMocks) + + mountMocks.foo2Id.mockClear() + mountMocks.bar2Id.mockClear() + await act(() => fireEvent.click(links.foo_2_1_bar2_1)) + const renderCount = await screen.findByTestId('foo2Id-Render-Count') + + await check('foo2Id', { value: '1', mountCount: 1 }, mountMocks) + await check('bar2Id', { value: '1', mountCount: 1 }, mountMocks) + expect(renderCount).toBeInTheDocument() + expect(renderCount).toHaveTextContent('1') + + await act(() => fireEvent.click(links.foo_2_1_bar2_2)) + await check('foo2Id', { value: '1', mountCount: 1 }, mountMocks) + await check('bar2Id', { value: '2', mountCount: 2 }, mountMocks) + expect(renderCount).toBeInTheDocument() + expect(renderCount).toHaveTextContent('1') + + mountMocks.foo2Id.mockClear() + mountMocks.bar2Id.mockClear() + + await act(() => fireEvent.click(links.foo_2_2_bar2_1)) + await check('foo2Id', { value: '2', mountCount: 1 }, mountMocks) + await check('bar2Id', { value: '1', mountCount: 1 }, mountMocks) + expect(renderCount).toBeInTheDocument() + expect(renderCount).toHaveTextContent('2') }) it('should remount the fooId and barId page component when navigating to the same route but different path param if defaultRemountDeps with params is used', async () => { diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 827f17c2432..7402001cdf4 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -403,6 +403,10 @@ export function interpolatePath({ if (segment.type === SEGMENT_TYPE_WILDCARD) { usedParams._splat = params._splat + + // TODO: Deprecate * + usedParams['*'] = params._splat + const segmentPrefix = segment.prefixSegment || '' const segmentSuffix = segment.suffixSegment || '' diff --git a/packages/solid-router/src/useParams.tsx b/packages/solid-router/src/useParams.tsx index ea00dc62309..1788f69fe94 100644 --- a/packages/solid-router/src/useParams.tsx +++ b/packages/solid-router/src/useParams.tsx @@ -62,10 +62,12 @@ export function useParams< > { return useMatch({ from: opts.from!, - strict: opts.strict, shouldThrow: opts.shouldThrow, - select: (match: any) => { - return opts.select ? opts.select(match.params) : match.params + strict: opts.strict, + select: (match) => { + const params = opts.strict === false ? match.params : match._strictParams + + return opts.select ? opts.select(params) : params }, - } as any) as any + }) as Accessor }