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 28197e4d987..56301f56e1f 100644 --- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts @@ -41,6 +41,7 @@ import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(anoth import { Route as RelativeUseNavigateRouteRouteImport } from './routes/relative/useNavigate/route' import { Route as RelativeLinkRouteRouteImport } from './routes/relative/link/route' import { Route as PathlessLayoutLayoutRouteRouteImport } from './routes/pathless-layout/_layout/route' +import { Route as ParamsPsStrictFalseRouteRouteImport } from './routes/params-ps/strict-false/route' import { Route as ParamsPsNonNestedRouteRouteImport } from './routes/params-ps/non-nested/route' import { Route as NonNestedSuffixRouteRouteImport } from './routes/non-nested/suffix/route' import { Route as NonNestedPrefixRouteRouteImport } from './routes/non-nested/prefix/route' @@ -78,6 +79,7 @@ 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 ParamsPsStrictFalseVersionRouteRouteImport } from './routes/params-ps/strict-false/$version.route' import { Route as ParamsPsNonNestedFooRouteRouteImport } from './routes/params-ps/non-nested/$foo_/route' import { Route as ParamsPsNamedFooRouteRouteImport } from './routes/params-ps/named/$foo/route' import { Route as NonNestedSuffixChar123bazChar125suffixRouteRouteImport } from './routes/non-nested/suffix/{$baz}suffix.route' @@ -281,6 +283,12 @@ const PathlessLayoutLayoutRouteRoute = id: '/_layout', getParentRoute: () => PathlessLayoutRouteRoute, } as any) +const ParamsPsStrictFalseRouteRoute = + ParamsPsStrictFalseRouteRouteImport.update({ + id: '/params-ps/strict-false', + path: '/params-ps/strict-false', + getParentRoute: () => rootRouteImport, + } as any) const ParamsPsNonNestedRouteRoute = ParamsPsNonNestedRouteRouteImport.update({ id: '/params-ps/non-nested', path: '/params-ps/non-nested', @@ -481,6 +489,12 @@ const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({ path: '/insidelayout', getParentRoute: () => groupLayoutRoute, } as any) +const ParamsPsStrictFalseVersionRouteRoute = + ParamsPsStrictFalseVersionRouteRouteImport.update({ + id: '/$version', + path: '/$version', + getParentRoute: () => ParamsPsStrictFalseRouteRoute, + } as any) const ParamsPsNonNestedFooRouteRoute = ParamsPsNonNestedFooRouteRouteImport.update({ id: '/$foo_', @@ -721,6 +735,7 @@ export interface FileRoutesByFullPath { '/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren '/non-nested/suffix': typeof NonNestedSuffixRouteRouteWithChildren '/params-ps/non-nested': typeof ParamsPsNonNestedRouteRouteWithChildren + '/params-ps/strict-false': typeof ParamsPsStrictFalseRouteRouteWithChildren '/relative/link': typeof RelativeLinkRouteRouteWithChildren '/relative/useNavigate': typeof RelativeUseNavigateRouteRouteWithChildren '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute @@ -742,6 +757,7 @@ export interface FileRoutesByFullPath { '/non-nested/suffix/{$baz}suffix': typeof NonNestedSuffixChar123bazChar125suffixRouteRouteWithChildren '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/params-ps/non-nested/$foo': typeof ParamsPsNonNestedFooRouteRouteWithChildren + '/params-ps/strict-false/$version': typeof ParamsPsStrictFalseVersionRouteRoute '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute @@ -825,6 +841,7 @@ export interface FileRoutesByTo { '/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren '/non-nested/suffix': typeof NonNestedSuffixRouteRouteWithChildren '/params-ps/non-nested': typeof ParamsPsNonNestedRouteRouteWithChildren + '/params-ps/strict-false': typeof ParamsPsStrictFalseRouteRouteWithChildren '/relative/link': typeof RelativeLinkRouteRouteWithChildren '/relative/useNavigate': typeof RelativeUseNavigateRouteRouteWithChildren '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute @@ -840,6 +857,7 @@ export interface FileRoutesByTo { '/search-params': typeof SearchParamsIndexRoute '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/params-ps/non-nested/$foo': typeof ParamsPsNonNestedFooRouteRouteWithChildren + '/params-ps/strict-false/$version': typeof ParamsPsStrictFalseVersionRouteRoute '/insidelayout': typeof groupLayoutInsidelayoutRoute '/subfolder/inside': typeof groupSubfolderInsideRoute '/layout-a': typeof LayoutLayout2LayoutARoute @@ -924,6 +942,7 @@ export interface FileRoutesById { '/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren '/non-nested/suffix': typeof NonNestedSuffixRouteRouteWithChildren '/params-ps/non-nested': typeof ParamsPsNonNestedRouteRouteWithChildren + '/params-ps/strict-false': typeof ParamsPsStrictFalseRouteRouteWithChildren '/pathless-layout/_layout': typeof PathlessLayoutLayoutRouteRouteWithChildren '/relative/link': typeof RelativeLinkRouteRouteWithChildren '/relative/useNavigate': typeof RelativeUseNavigateRouteRouteWithChildren @@ -948,6 +967,7 @@ export interface FileRoutesById { '/non-nested/suffix/{$baz}suffix': typeof NonNestedSuffixChar123bazChar125suffixRouteRouteWithChildren '/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren '/params-ps/non-nested/$foo_': typeof ParamsPsNonNestedFooRouteRouteWithChildren + '/params-ps/strict-false/$version': typeof ParamsPsStrictFalseVersionRouteRoute '/(group)/_layout/insidelayout': typeof groupLayoutInsidelayoutRoute '/(group)/subfolder/inside': typeof groupSubfolderInsideRoute '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute @@ -1035,6 +1055,7 @@ export interface FileRouteTypes { | '/non-nested/prefix' | '/non-nested/suffix' | '/params-ps/non-nested' + | '/params-ps/strict-false' | '/relative/link' | '/relative/useNavigate' | '/onlyrouteinside' @@ -1056,6 +1077,7 @@ export interface FileRouteTypes { | '/non-nested/suffix/{$baz}suffix' | '/params-ps/named/$foo' | '/params-ps/non-nested/$foo' + | '/params-ps/strict-false/$version' | '/insidelayout' | '/subfolder/inside' | '/layout-a' @@ -1139,6 +1161,7 @@ export interface FileRouteTypes { | '/non-nested/prefix' | '/non-nested/suffix' | '/params-ps/non-nested' + | '/params-ps/strict-false' | '/relative/link' | '/relative/useNavigate' | '/onlyrouteinside' @@ -1154,6 +1177,7 @@ export interface FileRouteTypes { | '/search-params' | '/params-ps/named/$foo' | '/params-ps/non-nested/$foo' + | '/params-ps/strict-false/$version' | '/insidelayout' | '/subfolder/inside' | '/layout-a' @@ -1237,6 +1261,7 @@ export interface FileRouteTypes { | '/non-nested/prefix' | '/non-nested/suffix' | '/params-ps/non-nested' + | '/params-ps/strict-false' | '/pathless-layout/_layout' | '/relative/link' | '/relative/useNavigate' @@ -1261,6 +1286,7 @@ export interface FileRouteTypes { | '/non-nested/suffix/{$baz}suffix' | '/params-ps/named/$foo' | '/params-ps/non-nested/$foo_' + | '/params-ps/strict-false/$version' | '/(group)/_layout/insidelayout' | '/(group)/subfolder/inside' | '/_layout/_layout-2/layout-a' @@ -1343,6 +1369,7 @@ export interface RootRouteChildren { PostsRoute: typeof PostsRouteWithChildren RemountDepsRoute: typeof RemountDepsRoute ParamsPsNonNestedRouteRoute: typeof ParamsPsNonNestedRouteRouteWithChildren + ParamsPsStrictFalseRouteRoute: typeof ParamsPsStrictFalseRouteRouteWithChildren RelativeLinkRouteRoute: typeof RelativeLinkRouteRouteWithChildren RelativeUseNavigateRouteRoute: typeof RelativeUseNavigateRouteRouteWithChildren anotherGroupOnlyrouteinsideRoute: typeof anotherGroupOnlyrouteinsideRoute @@ -1598,6 +1625,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PathlessLayoutLayoutRouteRouteImport parentRoute: typeof PathlessLayoutRouteRoute } + '/params-ps/strict-false': { + id: '/params-ps/strict-false' + path: '/params-ps/strict-false' + fullPath: '/params-ps/strict-false' + preLoaderRoute: typeof ParamsPsStrictFalseRouteRouteImport + parentRoute: typeof rootRouteImport + } '/params-ps/non-nested': { id: '/params-ps/non-nested' path: '/params-ps/non-nested' @@ -1857,6 +1891,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport parentRoute: typeof groupLayoutRoute } + '/params-ps/strict-false/$version': { + id: '/params-ps/strict-false/$version' + path: '/$version' + fullPath: '/params-ps/strict-false/$version' + preLoaderRoute: typeof ParamsPsStrictFalseVersionRouteRouteImport + parentRoute: typeof ParamsPsStrictFalseRouteRoute + } '/params-ps/non-nested/$foo_': { id: '/params-ps/non-nested/$foo_' path: '/$foo' @@ -2472,6 +2513,20 @@ const ParamsPsNonNestedRouteRouteWithChildren = ParamsPsNonNestedRouteRouteChildren, ) +interface ParamsPsStrictFalseRouteRouteChildren { + ParamsPsStrictFalseVersionRouteRoute: typeof ParamsPsStrictFalseVersionRouteRoute +} + +const ParamsPsStrictFalseRouteRouteChildren: ParamsPsStrictFalseRouteRouteChildren = + { + ParamsPsStrictFalseVersionRouteRoute: ParamsPsStrictFalseVersionRouteRoute, + } + +const ParamsPsStrictFalseRouteRouteWithChildren = + ParamsPsStrictFalseRouteRoute._addFileChildren( + ParamsPsStrictFalseRouteRouteChildren, + ) + interface RelativeLinkRouteRouteChildren { RelativeLinkRelativeLinkARoute: typeof RelativeLinkRelativeLinkARoute RelativeLinkRelativeLinkBRoute: typeof RelativeLinkRelativeLinkBRoute @@ -2599,6 +2654,7 @@ const rootRouteChildren: RootRouteChildren = { PostsRoute: PostsRouteWithChildren, RemountDepsRoute: RemountDepsRoute, ParamsPsNonNestedRouteRoute: ParamsPsNonNestedRouteRouteWithChildren, + ParamsPsStrictFalseRouteRoute: ParamsPsStrictFalseRouteRouteWithChildren, RelativeLinkRouteRoute: RelativeLinkRouteRouteWithChildren, RelativeUseNavigateRouteRoute: RelativeUseNavigateRouteRouteWithChildren, anotherGroupOnlyrouteinsideRoute: anotherGroupOnlyrouteinsideRoute, diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/index.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/index.tsx index 02235bc3daf..b8976b901b5 100644 --- a/e2e/react-router/basic-file-based/src/routes/params-ps/index.tsx +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/index.tsx @@ -122,6 +122,28 @@ function RouteComponent() { +
+

Parsed params with strict false

+ ) } diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/$version.route.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/$version.route.tsx new file mode 100644 index 00000000000..6803de8c9b5 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/$version.route.tsx @@ -0,0 +1,21 @@ +import { createFileRoute, useParams } from '@tanstack/react-router' + +export const Route = createFileRoute('/params-ps/strict-false/$version')({ + params: { + parse: (params) => ({ + ...params, + version: parseInt(params.version), + }), + stringify: (params) => ({ + ...params, + version: `${params.version}`, + }), + }, + component: RouteComponent, +}) + +function RouteComponent() { + const { version } = useParams({ strict: false }) + + return
{String(version)}
+} diff --git a/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/route.tsx b/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/route.tsx new file mode 100644 index 00000000000..f9f96cb7da8 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/params-ps/strict-false/route.tsx @@ -0,0 +1,44 @@ +import { + Link, + Outlet, + createFileRoute, + useParams, +} from '@tanstack/react-router' + +export const Route = createFileRoute('/params-ps/strict-false')({ + component: RouteComponent, +}) + +function RouteComponent() { + const { version } = useParams({ strict: false }) + return ( +
+

ParamsStrictFalseParse

+
+ Type:{' '} + {typeof version} +
+
+ Value:{' '} + {String(version)} +
+ + Version 1 + + + Version 2 + + +
+ ) +} 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 6b43dba314e..a867696ff83 100644 --- a/e2e/react-router/basic-file-based/tests/params.spec.ts +++ b/e2e/react-router/basic-file-based/tests/params.spec.ts @@ -492,3 +492,29 @@ test.describe('Unicode params', () => { }) }) }) + +test.describe('useParams strict false uses parsed child params', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/params-ps') + }) + + test('parent receives parsed values after child navigation', async ({ + page, + }) => { + await page.getByTestId('strict-false-version-1').click() + await page.waitForURL('/params-ps/strict-false/1') + + await expect(page.getByTestId('strict-false-version-type')).toHaveText( + 'number', + ) + await expect(page.getByTestId('strict-false-version-value')).toHaveText('1') + + await page.getByTestId('strict-false-version-2').click() + await page.waitForURL('/params-ps/strict-false/2') + + await expect(page.getByTestId('strict-false-version-type')).toHaveText( + 'number', + ) + await expect(page.getByTestId('strict-false-version-value')).toHaveText('2') + }) +}) diff --git a/packages/react-router/tests/useParams.test.tsx b/packages/react-router/tests/useParams.test.tsx index ec54c5e1fb1..1d1e1ae3a32 100644 --- a/packages/react-router/tests/useParams.test.tsx +++ b/packages/react-router/tests/useParams.test.tsx @@ -248,3 +248,73 @@ test('useParams must return parsed result if applicable.', async () => { expect(paramPostIdValue.textContent).toBe('2') expect(mockedfn).toHaveBeenCalledTimes(2) }) + +test('useParams({ strict: false }) returns parsed params after child navigation', async () => { + const rootRoute = createRootRoute() + + const parentRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'parent', + component: ParentComponent, + }) + + const versionRoute = createRoute({ + getParentRoute: () => parentRoute, + path: '$version', + params: { + parse: (params) => ({ + ...params, + version: parseInt(params.version), + }), + stringify: (params) => ({ + ...params, + version: `${params.version}`, + }), + }, + component: VersionComponent, + }) + + function ParentComponent() { + const { version } = useParams({ strict: false }) + + return ( +
+
{typeof version}
+
{String(version)}
+ + Version 2 + + +
+ ) + } + + function VersionComponent() { + return
Version Route
+ } + + window.history.replaceState({}, '', '/parent/1') + + const router = createRouter({ + routeTree: rootRoute.addChildren([parentRoute.addChildren([versionRoute])]), + }) + + render() + + await act(() => router.load()) + + expect(await screen.findByTestId('version-type')).toHaveTextContent('number') + expect(await screen.findByTestId('version-value')).toHaveTextContent('1') + + const version2Link = await screen.findByTestId('version-2-link') + + await act(() => fireEvent.click(version2Link)) + + expect(await screen.findByTestId('version-route')).toBeInTheDocument() + expect(await screen.findByTestId('version-type')).toHaveTextContent('number') + expect(await screen.findByTestId('version-value')).toHaveTextContent('2') +}) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 67ae9524cfc..0cae2c9727c 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1304,6 +1304,9 @@ export class RouterCore< })() const matches: Array = [] + const previousMatchesByRouteId = new Map( + this.state.matches.map((match) => [match.routeId, match]), + ) const getParentContext = (parentMatch?: AnyRouteMatch) => { const parentMatchId = parentMatch?.id @@ -1397,9 +1400,7 @@ export class RouterCore< const existingMatch = this.getMatch(matchId) - const previousMatch = this.state.matches.find( - (d) => d.routeId === route.id, - ) + const previousMatch = previousMatchesByRouteId.get(route.id) const strictParams = existingMatch?._strictParams ?? usedParams @@ -1533,8 +1534,13 @@ export class RouterCore< const route = this.looseRoutesById[match.routeId]! const existingMatch = this.getMatch(match.id) - // only execute `context` if we are not calling from router.buildLocation + // Update the match's params + const previousMatch = previousMatchesByRouteId.get(match.routeId) + match.params = previousMatch + ? replaceEqualDeep(previousMatch.params, routeParams) + : routeParams + // only execute `context` if we are not calling from router.buildLocation if (!existingMatch && opts?._buildLocation !== true) { const parentMatch = matches[index - 1] const parentContext = getParentContext(parentMatch)