diff --git a/packages/react-router/tests/useParams.test.tsx b/packages/react-router/tests/useParams.test.tsx index ec54c5e1fb1..3c15aade319 100644 --- a/packages/react-router/tests/useParams.test.tsx +++ b/packages/react-router/tests/useParams.test.tsx @@ -189,7 +189,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_first') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(1) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await act(() => fireEvent.click(firstPostLink)) @@ -213,7 +213,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('one') expect(paramCategoryValue.textContent).toBe('one') expect(paramPostIdValue.textContent).toBe('1') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) expect(allCategoryLink).toBeInTheDocument() mockedfn.mockClear() @@ -224,7 +224,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_all') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() expect(secondPostLink).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await act(() => fireEvent.click(secondPostLink)) @@ -246,5 +246,5 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('two') expect(paramCategoryValue.textContent).toBe('all') expect(paramPostIdValue.textContent).toBe('2') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) }) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index f0c20f4170b..6ac72354975 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1330,22 +1330,9 @@ export class RouterCore< } } - globalNotFoundRouteId = (() => { - if (!isGlobalNotFound) { - return undefined - } - - if (this.options.notFoundMode !== 'root') { - for (let i = matchedRoutes.length - 1; i >= 0; i--) { - const route = matchedRoutes[i]! - if (route.children) { - return route.id - } - } - } - - return rootRouteId - })() + globalNotFoundRouteId = isGlobalNotFound + ? findGlobalNotFoundRouteId(this.options.notFoundMode, matchedRoutes) + : undefined } const matches: Array = [] @@ -1775,16 +1762,30 @@ export class RouterCore< params: nextParams, }).interpolatedPath - const { matches: destMatches, rawParams } = this.matchRoutesInternal( - { pathname: interpolatedNextTo } as ParsedLocation, - { _buildLocation: true }, - ) - const destRoutes = destMatches.map( - (d) => this.looseRoutesById[d.routeId]!, - ) - - // Check if any match indicates global not found - const globalNotFoundMatch = destMatches.find((m) => m.globalNotFound) + // Use lightweight getMatchedRoutes instead of matchRoutesInternal + // This avoids creating full match objects (AbortController, ControlledPromise, etc.) + // which are expensive and not needed for buildLocation + const destMatchResult = this.getMatchedRoutes(interpolatedNextTo) + let destRoutes = destMatchResult.matchedRoutes + const rawParams = destMatchResult.routeParams + + // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal + const isGlobalNotFound = destMatchResult.foundRoute + ? destMatchResult.foundRoute.path !== '/' && + destMatchResult.routeParams['**'] + : trimPathRight(interpolatedNextTo) + + let globalNotFoundRouteId: string | undefined + if (isGlobalNotFound) { + if (this.options.notFoundRoute) { + destRoutes = [...destRoutes, this.options.notFoundRoute] + } else { + globalNotFoundRouteId = findGlobalNotFoundRouteId( + this.options.notFoundMode, + destRoutes, + ) + } + } // If there are any params, we need to stringify them if (Object.keys(nextParams).length > 0) { @@ -1877,7 +1878,7 @@ export class RouterCore< routes: destRoutes, params: snapshotParams, searchStr, - globalNotFoundRouteId: globalNotFoundMatch?.routeId, + globalNotFoundRouteId, }) // Create the full path of the location @@ -3045,7 +3046,7 @@ function applySearchMiddleware({ }: { search: any dest: BuildNextOptions - destRoutes: Array + destRoutes: ReadonlyArray _includeValidateSearch: boolean | undefined }) { const allMiddlewares = @@ -3151,3 +3152,18 @@ function applySearchMiddleware({ // Start applying middlewares return applyNext(0, search) } + +function findGlobalNotFoundRouteId( + notFoundMode: 'root' | 'fuzzy' | undefined, + routes: ReadonlyArray, +) { + if (notFoundMode !== 'root') { + for (let i = routes.length - 1; i >= 0; i--) { + const route = routes[i]! + if (route.children) { + return route.id + } + } + } + return rootRouteId +} diff --git a/packages/solid-router/tests/useParams.test.tsx b/packages/solid-router/tests/useParams.test.tsx index 291e76d0f81..b94df57c6ae 100644 --- a/packages/solid-router/tests/useParams.test.tsx +++ b/packages/solid-router/tests/useParams.test.tsx @@ -188,7 +188,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_first') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(1) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await waitFor(() => fireEvent.click(firstPostLink)) @@ -211,7 +211,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('one') expect(paramCategoryValue.textContent).toBe('one') expect(paramPostIdValue.textContent).toBe('1') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) expect(allCategoryLink).toBeInTheDocument() mockedfn.mockClear() @@ -222,7 +222,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_all') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() expect(secondPostLink).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await waitFor(() => fireEvent.click(secondPostLink)) @@ -244,5 +244,5 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('two') expect(paramCategoryValue.textContent).toBe('all') expect(paramPostIdValue.textContent).toBe('2') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) }) diff --git a/packages/vue-router/tests/useParams.test.tsx b/packages/vue-router/tests/useParams.test.tsx index 897e43928f0..07da6c02d14 100644 --- a/packages/vue-router/tests/useParams.test.tsx +++ b/packages/vue-router/tests/useParams.test.tsx @@ -193,7 +193,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_first') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(1) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await waitFor(() => fireEvent.click(firstPostLink)) @@ -216,7 +216,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('one') expect(paramCategoryValue.textContent).toBe('one') expect(paramPostIdValue.textContent).toBe('1') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) expect(allCategoryLink).toBeInTheDocument() mockedfn.mockClear() @@ -227,7 +227,7 @@ test('useParams must return parsed result if applicable.', async () => { expect(window.location.pathname).toBe('/posts/category_all') expect(await screen.findByTestId('post-category-heading')).toBeInTheDocument() expect(secondPostLink).toBeInTheDocument() - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).not.toHaveBeenCalled() mockedfn.mockClear() await waitFor(() => fireEvent.click(secondPostLink)) @@ -249,5 +249,5 @@ test('useParams must return parsed result if applicable.', async () => { expect(renderedPost.category).toBe('two') expect(paramCategoryValue.textContent).toBe('all') expect(paramPostIdValue.textContent).toBe('2') - expect(mockedfn).toHaveBeenCalledTimes(2) + expect(mockedfn).toHaveBeenCalledTimes(1) })