Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 34 additions & 31 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,17 @@ export interface MatchRoutesFn {
): Array<AnyRouteMatch>
}

type MatchRoutesLightweightResult = {
matchedRoutes: ReadonlyArray<AnyRoute>
fullPath: string
params: Record<string, unknown>
}

type MatchRoutesLightweightCache = {
location: ParsedLocation
result: MatchRoutesLightweightResult
}

export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined

export type UpdateMatchFn = (
Expand Down Expand Up @@ -1674,44 +1685,28 @@ export class RouterCore<
})
}

private matchRoutesLightweightCache?: MatchRoutesLightweightCache
/**
* Lightweight route matching for buildLocation.
* Only computes fullPath, accumulated search, and params - skipping expensive
* operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
*/
private matchRoutesLightweight(location: ParsedLocation): {
matchedRoutes: ReadonlyArray<AnyRoute>
fullPath: string
search: Record<string, unknown>
params: Record<string, unknown>
} {
private matchRoutesLightweight(
location: ParsedLocation,
): MatchRoutesLightweightResult {
const isCurrent = this.stores.location.state === location
if (isCurrent) {
const cached = this.matchRoutesLightweightCache
if (cached?.location === location) {
return cached.result
}
}

const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
location.pathname,
)
const lastRoute = last(matchedRoutes)!

// I don't know if we should run the full search middleware chain, or just validateSearch
// // Accumulate search validation through the route chain
// const accumulatedSearch: Record<string, unknown> = applySearchMiddleware({
// search: { ...location.search },
// dest: location,
// destRoutes: matchedRoutes,
// _includeValidateSearch: true,
// })

// Accumulate search validation through route chain
const accumulatedSearch = { ...location.search }
for (const route of matchedRoutes) {
try {
Object.assign(
accumulatedSearch,
validateSearch(route.options.validateSearch, accumulatedSearch),
)
} catch {
// Ignore errors, we're not actually routing
}
}

// Determine params: reuse from state if possible, otherwise parse
const lastStateMatchId = last(this.stores.matchesId.state)
const lastStateMatch =
Expand Down Expand Up @@ -1746,12 +1741,20 @@ export class RouterCore<
params = strictParams
}

return {
const result = {
matchedRoutes,
fullPath: lastRoute.fullPath,
search: accumulatedSearch,
params,
}

if (isCurrent) {
this.matchRoutesLightweightCache = {
location,
result,
}
}

return result
}

cancelMatch = (id: string) => {
Expand Down Expand Up @@ -1839,7 +1842,7 @@ export class RouterCore<
const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')

// From search should always use the current location
const fromSearch = lightweightResult.search
const fromSearch = currentLocation.search
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve validated search when using _fromLocation

When buildLocation is driven from an _fromLocation (render-time <Link>s do this via packages/react-router/src/link.tsx:407, and preloadRoute retries redirects with _fromLocation: next in router.ts:2863-2866), currentLocation.search is only the raw parsed query string, not the route-validated search state. Before this change matchRoutesLightweight re-ran validateSearch, so defaults like validateSearch: () => ({ postPage: 0 }) were still visible to search updaters even when the URL omitted ?postPage=. Now those updaters receive {} instead, which breaks relative links and redirect/preload flows that rely on inherited defaults (for example the redirect-preload case covered in packages/react-router/tests/link.test.tsx:3899-4020).

Useful? React with 👍 / 👎.

// Same with params. It can't hurt to provide as many as possible
const fromParams = Object.assign(
Object.create(null),
Expand Down
Loading