diff --git a/packages/react-router/tests/useParams.test.tsx b/packages/react-router/tests/useParams.test.tsx
index 8aa2618bf6c..ec54c5e1fb1 100644
--- a/packages/react-router/tests/useParams.test.tsx
+++ b/packages/react-router/tests/useParams.test.tsx
@@ -1,4 +1,4 @@
-import { expect, test } from 'vitest'
+import { expect, test, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import {
Link,
@@ -24,6 +24,7 @@ test('useParams must return parsed result if applicable.', async () => {
},
]
+ const mockedfn = vi.fn()
const rootRoute = createRootRoute()
const postsRoute = createRoute({
@@ -70,23 +71,19 @@ test('useParams must return parsed result if applicable.', async () => {
const postRoute = createRoute({
getParentRoute: () => postCategoryRoute,
path: '$postId',
+ loader: ({ params }) => {
+ return { post: posts.find((post) => post.id === parseInt(params.postId)) }
+ },
params: {
parse: (params) => {
+ mockedfn()
return {
...params,
- id: params.postId === 'one' ? 1 : 2,
- }
- },
- stringify: (params) => {
- return {
- postId: params.id === 1 ? 'one' : 'two',
+ postId: params.postId === 'one' ? '1' : '2',
}
},
},
component: PostComponent,
- loader: ({ params }) => ({
- post: posts.find((post) => post.id === params.id),
- }),
})
function PostsComponent() {
@@ -124,7 +121,8 @@ test('useParams must return parsed result if applicable.', async () => {
{post.title}
@@ -152,9 +150,6 @@ test('useParams must return parsed result if applicable.', async () => {
PostId_Param:{' '}
{params.postId}
-
- Id_Param: {params.id}
-
PostId: {data.post.id}
@@ -187,75 +182,69 @@ test('useParams must return parsed result if applicable.', async () => {
expect(firstCategoryLink).toBeInTheDocument()
+ mockedfn.mockClear()
await act(() => fireEvent.click(firstCategoryLink))
- expect(window.location.pathname).toBe('/posts/category_first')
-
- const postCategoryHeading = await screen.findByTestId('post-category-heading')
const firstPostLink = await screen.findByTestId('post-one-link')
- expect(postCategoryHeading).toBeInTheDocument()
+ expect(window.location.pathname).toBe('/posts/category_first')
+ expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument()
+ expect(mockedfn).toHaveBeenCalledTimes(1)
- fireEvent.click(firstPostLink)
+ mockedfn.mockClear()
+ await act(() => fireEvent.click(firstPostLink))
- let postHeading = await screen.findByTestId('post-heading')
+ const allCategoryLink = await screen.findByTestId('all-category-link')
let paramCategoryValue = await screen.findByTestId('param_category_value')
let paramPostIdValue = await screen.findByTestId('param_postId_value')
- let paramIdValue = await screen.findByTestId('param_id_value')
let postCategory = await screen.findByTestId('post_category_value')
let postTitleValue = await screen.findByTestId('post_title_value')
let postIdValue = await screen.findByTestId('post_id_value')
- expect(window.location.pathname).toBe('/posts/category_first/one')
- expect(postHeading).toBeInTheDocument()
-
let renderedPost = {
id: parseInt(postIdValue.textContent),
title: postTitleValue.textContent,
category: postCategory.textContent,
}
+ expect(window.location.pathname).toBe('/posts/category_first/one')
+ expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
expect(renderedPost).toEqual(posts[0])
expect(renderedPost.category).toBe('one')
expect(paramCategoryValue.textContent).toBe('one')
- expect(paramPostIdValue.textContent).toBe('one')
- expect(paramIdValue.textContent).toBe('1')
-
- const allCategoryLink = await screen.findByTestId('all-category-link')
-
+ expect(paramPostIdValue.textContent).toBe('1')
+ expect(mockedfn).toHaveBeenCalledTimes(2)
expect(allCategoryLink).toBeInTheDocument()
+ mockedfn.mockClear()
await act(() => fireEvent.click(allCategoryLink))
- expect(window.location.pathname).toBe('/posts/category_all')
-
const secondPostLink = await screen.findByTestId('post-two-link')
- expect(postCategoryHeading).toBeInTheDocument()
+ expect(window.location.pathname).toBe('/posts/category_all')
+ expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument()
expect(secondPostLink).toBeInTheDocument()
+ expect(mockedfn).toHaveBeenCalledTimes(2)
- fireEvent.click(secondPostLink)
+ mockedfn.mockClear()
+ await act(() => fireEvent.click(secondPostLink))
- postHeading = await screen.findByTestId('post-heading')
paramCategoryValue = await screen.findByTestId('param_category_value')
paramPostIdValue = await screen.findByTestId('param_postId_value')
- paramIdValue = await screen.findByTestId('param_id_value')
postCategory = await screen.findByTestId('post_category_value')
postTitleValue = await screen.findByTestId('post_title_value')
postIdValue = await screen.findByTestId('post_id_value')
-
- expect(window.location.pathname).toBe('/posts/category_all/two')
- expect(postHeading).toBeInTheDocument()
-
renderedPost = {
id: parseInt(postIdValue.textContent),
title: postTitleValue.textContent,
category: postCategory.textContent,
}
+ expect(window.location.pathname).toBe('/posts/category_all/two')
+ expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
expect(renderedPost).toEqual(posts[1])
expect(renderedPost.category).toBe('two')
expect(paramCategoryValue.textContent).toBe('all')
- expect(paramPostIdValue.textContent).toBe('two')
- expect(paramIdValue.textContent).toBe('2')
+ expect(paramPostIdValue.textContent).toBe('2')
+ expect(mockedfn).toHaveBeenCalledTimes(2)
})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 00c3f8fd9bf..285b006c1f6 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1202,33 +1202,6 @@ export class RouterCore<
return rootRouteId
})()
- const parseErrors = matchedRoutes.map((route) => {
- let parsedParamsError
-
- const parseParams =
- route.options.params?.parse ?? route.options.parseParams
-
- if (parseParams) {
- try {
- const parsedParams = parseParams(routeParams)
- // Add the parsed params to the accumulated params bag
- Object.assign(routeParams, parsedParams)
- } catch (err: any) {
- parsedParamsError = new PathParamError(err.message, {
- cause: err,
- })
-
- if (opts?.throwOnError) {
- throw parsedParamsError
- }
-
- return parsedParamsError
- }
- }
-
- return
- })
-
const matches: Array = []
const getParentContext = (parentMatch?: AnyRouteMatch) => {
@@ -1315,20 +1288,36 @@ export class RouterCore<
parseCache: this.parsePathnameCache,
})
- const strictParams = interpolatePathResult.usedParams
+ // Waste not, want not. If we already have a match for this route,
+ // reuse it. This is important for layout routes, which might stick
+ // around between navigation actions that only change leaf routes.
+
+ // Existing matches are matches that are already loaded along with
+ // pending matches that are still loading
+ const matchId = interpolatePathResult.interpolatedPath + loaderDepsHash
+
+ const existingMatch = this.getMatch(matchId)
- let paramsError = parseErrors[index]
+ const previousMatch = this.state.matches.find(
+ (d) => d.routeId === route.id,
+ )
- const strictParseParams =
- route.options.params?.parse ?? route.options.parseParams
+ const strictParams =
+ existingMatch?._strictParams ?? interpolatePathResult.usedParams
- if (strictParseParams) {
- try {
- Object.assign(strictParams, strictParseParams(strictParams as any))
- } catch (err: any) {
- // any param errors should already have been dealt with above, if this
- // somehow differs, let's report this in the same manner
- if (!paramsError) {
+ let paramsError: PathParamError | undefined = undefined
+
+ if (!existingMatch) {
+ const strictParseParams =
+ route.options.params?.parse ?? route.options.parseParams
+
+ if (strictParseParams) {
+ try {
+ Object.assign(
+ strictParams,
+ strictParseParams(strictParams as Record),
+ )
+ } catch (err: any) {
paramsError = new PathParamError(err.message, {
cause: err,
})
@@ -1340,19 +1329,7 @@ export class RouterCore<
}
}
- // Waste not, want not. If we already have a match for this route,
- // reuse it. This is important for layout routes, which might stick
- // around between navigation actions that only change leaf routes.
-
- // Existing matches are matches that are already loaded along with
- // pending matches that are still loading
- const matchId = interpolatePathResult.interpolatedPath + loaderDepsHash
-
- const existingMatch = this.getMatch(matchId)
-
- const previousMatch = this.state.matches.find(
- (d) => d.routeId === route.id,
- )
+ Object.assign(routeParams, strictParams)
const cause = previousMatch ? 'stay' : 'enter'
@@ -1398,7 +1375,7 @@ export class RouterCore<
status,
isFetching: false,
error: undefined,
- paramsError: paramsError,
+ paramsError,
__routeContext: undefined,
_nonReactive: {
loadPromise: createControlledPromise(),
diff --git a/packages/solid-router/tests/useParams.test.tsx b/packages/solid-router/tests/useParams.test.tsx
index 8cab883e44f..291e76d0f81 100644
--- a/packages/solid-router/tests/useParams.test.tsx
+++ b/packages/solid-router/tests/useParams.test.tsx
@@ -1,4 +1,4 @@
-import { expect, test } from 'vitest'
+import { expect, test, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'
import {
Link,
@@ -24,6 +24,7 @@ test('useParams must return parsed result if applicable.', async () => {
},
]
+ const mockedfn = vi.fn()
const rootRoute = createRootRoute()
const postsRoute = createRoute({
@@ -72,20 +73,16 @@ test('useParams must return parsed result if applicable.', async () => {
path: '$postId',
params: {
parse: (params) => {
+ mockedfn()
return {
...params,
- id: params.postId === 'one' ? 1 : 2,
- }
- },
- stringify: (params) => {
- return {
- postId: params.id === 1 ? 'one' : 'two',
+ postId: params.postId === 'one' ? '1' : '2',
}
},
},
component: PostComponent,
loader: ({ params }) => ({
- post: posts.find((post) => post.id === params.id),
+ post: posts.find((post) => post.id === parseInt(params.postId)),
}),
})
@@ -123,7 +120,8 @@ test('useParams must return parsed result if applicable.', async () => {
return (
{post.title}
@@ -151,9 +149,6 @@ test('useParams must return parsed result if applicable.', async () => {
PostId_Param:{' '}
{params().postId}
-
- Id_Param: {params().id}
-
PostId: {data().post.id}
@@ -186,72 +181,68 @@ test('useParams must return parsed result if applicable.', async () => {
expect(firstCategoryLink).toBeInTheDocument()
+ mockedfn.mockClear()
await waitFor(() => fireEvent.click(firstCategoryLink))
- expect(window.location.pathname).toBe('/posts/category_first')
-
const firstPostLink = await screen.findByTestId('post-one-link')
+ expect(window.location.pathname).toBe('/posts/category_first')
expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument()
+ expect(mockedfn).toHaveBeenCalledTimes(1)
+ mockedfn.mockClear()
await waitFor(() => fireEvent.click(firstPostLink))
+ const allCategoryLink = await screen.findByTestId('all-category-link')
let paramCategoryValue = await screen.findByTestId('param_category_value')
let paramPostIdValue = await screen.findByTestId('param_postId_value')
- let paramIdValue = await screen.findByTestId('param_id_value')
let postCategory = await screen.findByTestId('post_category_value')
let postTitleValue = await screen.findByTestId('post_title_value')
let postIdValue = await screen.findByTestId('post_id_value')
-
- expect(window.location.pathname).toBe('/posts/category_first/one')
- expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
-
let renderedPost = {
id: parseInt(postIdValue.textContent),
title: postTitleValue.textContent,
category: postCategory.textContent,
}
+ expect(window.location.pathname).toBe('/posts/category_first/one')
+ expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
expect(renderedPost).toEqual(posts[0])
expect(renderedPost.category).toBe('one')
expect(paramCategoryValue.textContent).toBe('one')
- expect(paramPostIdValue.textContent).toBe('one')
- expect(paramIdValue.textContent).toBe('1')
-
- const allCategoryLink = await screen.findByTestId('all-category-link')
-
+ expect(paramPostIdValue.textContent).toBe('1')
+ expect(mockedfn).toHaveBeenCalledTimes(2)
expect(allCategoryLink).toBeInTheDocument()
+ mockedfn.mockClear()
await waitFor(() => fireEvent.click(allCategoryLink))
- expect(window.location.pathname).toBe('/posts/category_all')
-
const secondPostLink = await screen.findByTestId('post-two-link')
+ expect(window.location.pathname).toBe('/posts/category_all')
expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument()
expect(secondPostLink).toBeInTheDocument()
+ expect(mockedfn).toHaveBeenCalledTimes(2)
+ mockedfn.mockClear()
await waitFor(() => fireEvent.click(secondPostLink))
paramCategoryValue = await screen.findByTestId('param_category_value')
paramPostIdValue = await screen.findByTestId('param_postId_value')
- paramIdValue = await screen.findByTestId('param_id_value')
postCategory = await screen.findByTestId('post_category_value')
postTitleValue = await screen.findByTestId('post_title_value')
postIdValue = await screen.findByTestId('post_id_value')
-
- expect(window.location.pathname).toBe('/posts/category_all/two')
- expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
-
renderedPost = {
id: parseInt(postIdValue.textContent),
title: postTitleValue.textContent,
category: postCategory.textContent,
}
+ expect(window.location.pathname).toBe('/posts/category_all/two')
+ expect(await screen.findByTestId('post-heading')).toBeInTheDocument()
expect(renderedPost).toEqual(posts[1])
expect(renderedPost.category).toBe('two')
expect(paramCategoryValue.textContent).toBe('all')
- expect(paramPostIdValue.textContent).toBe('two')
- expect(paramIdValue.textContent).toBe('2')
+ expect(paramPostIdValue.textContent).toBe('2')
+ expect(mockedfn).toHaveBeenCalledTimes(2)
})