From 5b7f61dcaae62164d5b88701ec073246dcb5a212 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Wed, 21 Jan 2026 22:31:27 +0100 Subject: [PATCH 1/5] refactor(router-core): skip full matchRoutesInternal for Link's buildLocation, use simple getMatchedRoutes --- packages/router-core/src/router.ts | 44 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index f0c20f4170b..a341febc3c4 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1775,16 +1775,38 @@ 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) + const destRoutes = destMatchResult.matchedRoutes as Array + 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) { + globalNotFoundRouteId = this.options.notFoundRoute.id + } else if (this.options.notFoundMode !== 'root') { + for (let i = destRoutes.length - 1; i >= 0; i--) { + const route = destRoutes[i]! + if (route.children) { + globalNotFoundRouteId = route.id + break + } + } + if (!globalNotFoundRouteId) { + globalNotFoundRouteId = rootRouteId + } + } else { + globalNotFoundRouteId = rootRouteId + } + } // If there are any params, we need to stringify them if (Object.keys(nextParams).length > 0) { @@ -1877,7 +1899,7 @@ export class RouterCore< routes: destRoutes, params: snapshotParams, searchStr, - globalNotFoundRouteId: globalNotFoundMatch?.routeId, + globalNotFoundRouteId, }) // Create the full path of the location From 5704974ad1598b2bde3f87a0d7383fc62210418b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 22 Jan 2026 08:25:52 +0100 Subject: [PATCH 2/5] update unit tests, useParams not called on link building, only nav --- packages/react-router/tests/useParams.test.tsx | 8 ++++---- packages/solid-router/tests/useParams.test.tsx | 8 ++++---- packages/vue-router/tests/useParams.test.tsx | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) 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/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) }) From df7decf09cc2c4e94b063b56edb32236b035a581 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 22 Jan 2026 15:14:12 +0100 Subject: [PATCH 3/5] factorize a little --- packages/router-core/src/router.ts | 50 +++++++++++++----------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index a341febc3c4..6fbbdc3dd1d 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 = [] @@ -1792,19 +1779,11 @@ export class RouterCore< if (isGlobalNotFound) { if (this.options.notFoundRoute) { globalNotFoundRouteId = this.options.notFoundRoute.id - } else if (this.options.notFoundMode !== 'root') { - for (let i = destRoutes.length - 1; i >= 0; i--) { - const route = destRoutes[i]! - if (route.children) { - globalNotFoundRouteId = route.id - break - } - } - if (!globalNotFoundRouteId) { - globalNotFoundRouteId = rootRouteId - } } else { - globalNotFoundRouteId = rootRouteId + globalNotFoundRouteId = findGlobalNotFoundRouteId( + this.options.notFoundMode, + destRoutes, + ) } } @@ -3173,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 +} From 7de43c42c5590ec932e41f4c42626b37ff62720f Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 22 Jan 2026 15:17:27 +0100 Subject: [PATCH 4/5] remove type cast --- packages/router-core/src/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 6fbbdc3dd1d..f3061298901 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1766,7 +1766,7 @@ export class RouterCore< // This avoids creating full match objects (AbortController, ControlledPromise, etc.) // which are expensive and not needed for buildLocation const destMatchResult = this.getMatchedRoutes(interpolatedNextTo) - const destRoutes = destMatchResult.matchedRoutes as Array + const destRoutes = destMatchResult.matchedRoutes const rawParams = destMatchResult.routeParams // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal @@ -3046,7 +3046,7 @@ function applySearchMiddleware({ }: { search: any dest: BuildNextOptions - destRoutes: Array + destRoutes: ReadonlyArray _includeValidateSearch: boolean | undefined }) { const allMiddlewares = From 3e7e9a0f132f10e3803d8221c7dd9b274123e83b Mon Sep 17 00:00:00 2001 From: Sheraff Date: Thu, 22 Jan 2026 15:33:50 +0100 Subject: [PATCH 5/5] fix deprecated not found route handling --- packages/router-core/src/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index f3061298901..6ac72354975 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1766,7 +1766,7 @@ export class RouterCore< // This avoids creating full match objects (AbortController, ControlledPromise, etc.) // which are expensive and not needed for buildLocation const destMatchResult = this.getMatchedRoutes(interpolatedNextTo) - const destRoutes = destMatchResult.matchedRoutes + let destRoutes = destMatchResult.matchedRoutes const rawParams = destMatchResult.routeParams // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal @@ -1778,7 +1778,7 @@ export class RouterCore< let globalNotFoundRouteId: string | undefined if (isGlobalNotFound) { if (this.options.notFoundRoute) { - globalNotFoundRouteId = this.options.notFoundRoute.id + destRoutes = [...destRoutes, this.options.notFoundRoute] } else { globalNotFoundRouteId = findGlobalNotFoundRouteId( this.options.notFoundMode,