Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ceaa4d2
fix: lane match loader rewrite
Sheraff Jul 13, 2026
adf73dd
fix client/server build
Sheraff Jul 13, 2026
6f75f50
adopt preloaded beforeLoad
Sheraff Jul 13, 2026
f4b8934
fix adoption of server data
Sheraff Jul 13, 2026
7b5436b
fix internal error propagation, only user code is try/catch
Sheraff Jul 13, 2026
fb21a42
fix: solid Transitionner unsub cleanup before early return
Sheraff Jul 13, 2026
bf90602
fix: react-router not found ambiguous assertion
Sheraff Jul 13, 2026
f2ccae2
docs
Sheraff Jul 13, 2026
da5cb82
fix: preload always releases its borrowed lease
Sheraff Jul 13, 2026
cc21be0
error priority handling
Sheraff Jul 13, 2026
3a56878
Merge branch 'main' into fix-router-core-lane-match-loader
Sheraff Jul 13, 2026
2e8bbbe
Merge remote-tracking branch 'origin/main' into fix-router-core-lane-…
Sheraff Jul 13, 2026
af0f0c7
tests
Sheraff Jul 14, 2026
f54bf4b
fix #7673
Sheraff Jul 14, 2026
7be1149
ci: apply automated fixes
autofix-ci[bot] Jul 14, 2026
0ed58ea
rename consts to uppercase
Sheraff Jul 14, 2026
4fd731e
cleanup load-client
Sheraff Jul 14, 2026
acbfba3
fix load-client for undefined loaderData
Sheraff Jul 14, 2026
8fcf2eb
cleanup load-server
Sheraff Jul 14, 2026
3ba393e
ci: apply automated fixes
autofix-ci[bot] Jul 14, 2026
3d252e6
more cleanup
Sheraff Jul 14, 2026
4150cdf
ci: apply automated fixes
autofix-ci[bot] Jul 14, 2026
3ecc24b
update tests
Sheraff Jul 14, 2026
c801dcc
final coverage
Sheraff Jul 14, 2026
ded47d1
final fixed issue tests
Sheraff Jul 14, 2026
16dfcf9
ci: apply automated fixes
autofix-ci[bot] Jul 14, 2026
05efe8c
Merge remote-tracking branch 'origin/main' into fix-router-core-lane-…
Sheraff Jul 15, 2026
ba80053
minor byte shave in router-core
Sheraff Jul 15, 2026
125b13f
minor byte shave in Match
Sheraff Jul 15, 2026
2bba8fa
minor byte shaving
Sheraff Jul 15, 2026
1f50bd3
ci: apply automated fixes
autofix-ci[bot] Jul 15, 2026
cf40d59
comments and cleanup
Sheraff Jul 15, 2026
ec631eb
ci: apply automated fixes
autofix-ci[bot] Jul 15, 2026
b56cb51
test: does `declare` save any bytes?
Sheraff Jul 15, 2026
ef40259
test again, but i selected everything this time
Sheraff Jul 15, 2026
e177851
test: same, but property wasn't even initialized
Sheraff Jul 15, 2026
e7322f8
simplify Transitioner based on having a unique transitioner per router
Sheraff Jul 15, 2026
9810491
ci: apply automated fixes
autofix-ci[bot] Jul 15, 2026
45f7731
deprecated test
Sheraff Jul 15, 2026
a8d7c8c
no reset key on server
Sheraff Jul 15, 2026
f90018f
ci: apply automated fixes
autofix-ci[bot] Jul 15, 2026
f7f266b
some jsdoc
Sheraff Jul 15, 2026
5a8cc80
simplify getSemanticMatch
Sheraff Jul 15, 2026
1b20933
more jsdoc
Sheraff Jul 15, 2026
83fea0f
ci: apply automated fixes
autofix-ci[bot] Jul 15, 2026
1f4838d
beforeLoad context cleanup on error + naming + misc bugs
Sheraff Jul 15, 2026
4746633
server preload is no-op
Sheraff Jul 17, 2026
6683863
console.error over promise.reject for lifecycle errors
Sheraff Jul 17, 2026
3d68db6
INTERNALS update
Sheraff Jul 17, 2026
423f905
fix(router-core): complete lane match loader rewrite (#7858)
Sheraff Jul 20, 2026
7443d96
Merge remote-tracking branch 'origin/main' into fix-router-core-lane-…
Sheraff Jul 20, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ type RenderedEvent = {
}

type InterruptedNavigationRouter = {
latestLoadPromise: Promise<void> | undefined
load: () => Promise<void>
navigate: (options: {
to: '/fast/$id' | '/slow/$id'
Expand Down Expand Up @@ -104,18 +103,6 @@ function assertSlowNavigationSettlement(settlement: NavigationSettlement) {
)
}

async function awaitExpectedLoadSettlement(loadPromise: Promise<void>) {
try {
await loadPromise
} catch (reason) {
if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) {
return
}

throw reason
}
}

function reasonHasAbortShape(reason: unknown) {
return reason instanceof DOMException && reason.name === 'AbortError'
}
Expand Down Expand Up @@ -144,7 +131,6 @@ export function createWorkload(
let navigateFast: (id: string) => Promise<void> = uninitialized
let startSlowNavigation: (id: string) => Promise<NavigationSettlement> =
uninitializedSettlement
let getLatestLoadPromise: () => Promise<void> | undefined = () => undefined

function assertRenderedPage(page: 'shell' | 'fast', id?: string) {
const element = container?.querySelector<HTMLElement>('[data-bench-page]')
Expand Down Expand Up @@ -203,7 +189,6 @@ export function createWorkload(
const mounted = mountTestApp(container)
const router = mounted.router as InterruptedNavigationRouter
unmount = mounted.unmount
getLatestLoadPromise = () => router.latestLoadPromise

unsub = router.subscribe('onRendered', (event) => {
if (
Expand Down Expand Up @@ -265,7 +250,6 @@ export function createWorkload(
expectedRenderedPath = undefined
navigateFast = uninitialized
startSlowNavigation = uninitializedSettlement
getLatestLoadPromise = () => undefined
}

async function interrupt(
Expand All @@ -276,12 +260,6 @@ export function createWorkload(
const slowNavigation = startSlowNavigation(slowId)

await waitForSlowLoader(slowId)
const slowLoadPromise = getLatestLoadPromise()

if (!slowLoadPromise) {
throw new Error(`Slow navigation did not start a load for id: ${slowId}`)
}

await navigateFast(fastId)
resolveSlowLoader(slowId)

Expand All @@ -291,7 +269,6 @@ export function createWorkload(
assertSlowNavigationSettlement(settlement)
}

await awaitExpectedLoadSettlement(slowLoadPromise)
await drainMicrotasks()
}

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/memory/server/scenarios/aborted-requests/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ const textDecoder = new TextDecoder()
const abortedRequestModes: Record<Framework, AbortedRequestMode> = {
react: {
readMode: 'first-chunk',
cancelMode: 'plain',
cancelMode: 'swallow-abort-error',
},
solid: {
readMode: 'first-chunk',
cancelMode: 'plain',
cancelMode: 'swallow-abort-error',
},
vue: {
readMode: 'shell-before-deferred',
Expand Down
15 changes: 11 additions & 4 deletions docs/router/api/router/RouteMatchType.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@ interface RouteMatch {
routeId: string
pathname: string
params: Route['allParams']
status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
status: 'pending' | 'success' | 'error' | 'notFound'
isFetching: false | 'beforeLoad' | 'loader'
showPending: boolean
error: unknown
paramsError: unknown
searchError: unknown
updatedAt: number
loaderData?: Route['loaderData']
context: Route['allContext']
search: Route['fullSearchSchema']
fetchedAt: number
abortController: AbortController
cause: 'enter' | 'stay'
cause: 'preload' | 'enter' | 'stay'
ssr?: boolean | 'data-only'
}
```

`status` describes the match's render state. `isFetching` independently exposes
active `beforeLoad` or loader work. In particular, a successful match can report
`isFetching: 'loader'` while stale data remains visible during a background
reload.

The router state can contain matches below the pending, error, or not-found
boundary. Those matches remain observable as part of the structurally matched
lane even though the route renderer stops at the boundary.
16 changes: 10 additions & 6 deletions docs/router/api/router/RouteOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The `RouteOptions` type accepts an object with the following properties:
- Type: `(rawSearchParams: unknown) => TSearchSchema`
- Optional
- A function that will be called when this route is matched and passed the raw search params from the current location and return valid parsed search params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's search params and the return type will be inferred into the rest of the router.
- This is a planning callback. It must be deterministic and side-effect-free for the same input; do not navigate or mutate application/router state from it.
- Optionally, the parameter type can be tagged with the `SearchSchemaInput` type like this: `(searchParams: TSearchSchemaInput & SearchSchemaInput) => TSearchSchema`. If this tag is present, `TSearchSchemaInput` will be used to type the `search` property of `<Link />` and `navigate()` **instead of** `TSearchSchema`. The difference between `TSearchSchemaInput` and `TSearchSchema` can be useful, for example, to express optional search parameters.

### `search.middlewares` property
Expand All @@ -70,6 +71,7 @@ The `RouteOptions` type accepts an object with the following properties:
- Type: `(rawParams: Record<string, string>) => TParams`
- Optional
- A function that will be called when this route is matched and passed the raw params from the current location and return valid parsed params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's params and the return type will be inferred into the rest of the router.
- This is a planning callback. It must be deterministic and side-effect-free for the same input.

### `stringifyParams` method (⚠️ deprecated, use `params.stringify` instead)

Expand All @@ -82,6 +84,7 @@ The `RouteOptions` type accepts an object with the following properties:
- Type: `(rawParams: Record<string, string>) => TParams | false`
- Optional
- A function that will be called when this route is matched and passed the raw params from the current location and return valid parsed params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function returns parsed params, its return value will be used as the route's params and the return type will be inferred into the rest of the router.
- This is a planning callback. It must be deterministic and side-effect-free for the same input.
- Experimental: returning `false` during incoming route matching skips this route and allows matching to continue to another candidate route.

### `params.priority` property
Expand Down Expand Up @@ -176,6 +179,7 @@ type loaderDeps = (opts: { search: TFullSearchSchema }) => Record<string, any>

- Optional
- A function that will be called before this route is matched to provide additional unique identification to the route match and serve as a dependency tracker for when the match should be reloaded. It should return any serializable value that can uniquely identify the route match from navigation to navigation.
- This is a planning callback and cache-key function. It must be deterministic and side-effect-free for the same validated search input. The returned value and any serialization methods on it, such as `toJSON`, must also be side-effect-free.
- By default, path params are already used to uniquely identify a route match, so it's unnecessary to return these here.
- If your route match relies on search params for unique identification, it's required that you return them here so they can be made available in the `loader`'s `deps` argument.

Expand All @@ -191,14 +195,14 @@ type loaderDeps = (opts: { search: TFullSearchSchema }) => Record<string, any>
- Type: `number`
- Optional
- Defaults to `routerOptions.defaultPreloadStaleTime`, which defaults to `30_000` ms (30 seconds)
- The amount of time in milliseconds that a route match's loader data will be considered fresh when preloading. If a route match is preloaded again within this time frame, its loader data will not be reloaded. If a route match is loaded (for navigation) within this time frame, the normal `staleTime` is used instead.
- The amount of time in milliseconds that loader data produced by a preload is considered fresh. Another preload or the first navigation can reuse it within this interval. After navigation accepts the generation, subsequent freshness uses `staleTime`.

### `gcTime` property

- Type: `number`
- Optional
- Defaults to `routerOptions.defaultGcTime`, which defaults to 30 minutes.
- The amount of time in milliseconds that a route match's loader data will be kept in memory after a preload or it is no longer in use.
- Defaults to `routerOptions.defaultGcTime`, which defaults to 5 minutes.
- The amount of time in milliseconds that loader data from an ordinary load will be kept in memory after it is no longer in use.

### `shouldReload` property

Expand Down Expand Up @@ -234,12 +238,12 @@ type loaderDeps = (opts: { search: TFullSearchSchema }) => Record<string, any>
- Defaults to `routerOptions.defaultPendingMinMs` which defaults to `500`
- The minimum amount of time in milliseconds that the pending component will be shown for if it is shown. This is useful to prevent the pending component from flashing on the screen for a split second.

### `preloadMaxAge` property
### `preloadGcTime` property

- Type: `number`
- Optional
- Defaults to `30_000` ms (30 seconds)
- The maximum amount of time in milliseconds that a route's preloaded route data will be cached for. If a route is not matched within this time frame, its loader data will be discarded.
- Defaults to `routerOptions.defaultPreloadGcTime`, which defaults to 5 minutes.
- The amount of time in milliseconds that loader data produced by a preload can remain in memory while it is not in use. This controls retention; use `preloadStaleTime` to control whether retained data is fresh enough to reuse without reloading.

### `preSearchFilters` property (⚠️ deprecated, use `search.middlewares` instead)

Expand Down
4 changes: 2 additions & 2 deletions docs/router/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ The `RouterOptions` type accepts an object with the following properties and met

- Type: `number`
- Optional
- Defaults to `routerOptions.defaultGcTime`, which defaults to 30 minutes.
- Defaults to 5 minutes.
- The default `preloadGcTime` a route should use if no preloadGcTime is provided.

### `defaultGcTime` property

- Type: `number`
- Optional
- Defaults to 30 minutes.
- Defaults to 5 minutes.
- The default `gcTime` a route should use if no gcTime is provided.

### `defaultOnCatch` property
Expand Down
29 changes: 16 additions & 13 deletions docs/router/api/router/RouterStateType.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ id: RouterStateType
title: RouterState type
---

The `RouterState` type represents shape of the internal state of the router. The Router's internal state is useful, if you need to access certain internals of the router, such as any pending matches, is the router in its loading state, etc.
The `RouterState` type represents the observable state of the router, including
the requested and resolved locations, match presentation, and foreground loading
status.

```tsx
type RouterState = {
status: 'pending' | 'idle'
isLoading: boolean
isTransitioning: boolean
matches: Array<RouteMatch>
location: ParsedLocation
resolvedLocation: ParsedLocation
resolvedLocation?: ParsedLocation
}
```

Expand All @@ -23,22 +24,24 @@ The `RouterState` type contains all of the properties that are available on the
### `status` property

- Type: `'pending' | 'idle'`
- The current status of the router. If the router is pending, it means that it is currently loading a route or the router is still transitioning to the new route.
- The current foreground status of the router. `pending` means the requested route is still loading or waiting for its framework transition to settle.

### `isLoading` property

- Type: `boolean`
- `true` if the router is currently loading a route or waiting for a route to finish loading.

### `isTransitioning` property

- Type: `boolean`
- `true` if the router is currently transitioning to a new route.
- `true` when `status` is `pending`.
- Background loader refreshes do not change this value. Inspect each match's `isFetching` field to observe foreground `beforeLoad`/loader work and background loader work.

### `matches` property

- Type: [`Array<RouteMatch>`](./RouteMatchType.md)
- An array of all of the route matches that have been resolved and are currently active.
- The match presentation currently exposed to the application.
- A navigation can keep the previous presentation visible until pending UI is
published. Once the destination is presented, this contains its complete
structurally matched lane, including descendants that are still loading.
- Error and not-found results also preserve the complete structurally matched
lane. Rendering stops at the selected pending or terminal boundary; it does
not truncate this array.

### `location` property

Expand All @@ -47,5 +50,5 @@ The `RouterState` type contains all of the properties that are available on the

### `resolvedLocation` property

- Type: [`ParsedLocation`](./ParsedLocationType.md)
- The location that the router has resolved and loaded.
- Type: [`ParsedLocation`](./ParsedLocationType.md) | `undefined`
- The location whose load and framework transition have settled. It is `undefined` before the initial location resolves.
47 changes: 25 additions & 22 deletions docs/router/api/router/RouterType.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,6 @@ An instance of the `Router` has the following properties and methods:
- Matches a pathname and search params against the router's route tree and returns an array of route matches.
- If `opts.throwOnError` is `true`, any errors that occur during the matching process will be thrown (in addition to being returned in the route match's `error` property).

### `.cancelMatch` method

- Type: `(matchId: string) => void`
- Cancels a route match that is currently pending by calling `match.abortController.abort()`.

### `.cancelMatches` method

- Type: `() => void`
- Cancels all route matches that are currently pending by calling `match.abortController.abort()` on each one.

### `.buildLocation` method

Builds a new parsed location object that can be used later to navigate to a new location.
Expand Down Expand Up @@ -138,23 +128,27 @@ Navigates to a new location.

### `.invalidate` method

Invalidates route matches by forcing their `beforeLoad` and `load` functions to be called again.
Invalidates selected route-match generations, reruns their loading lifecycle,
reruns `beforeLoad`, and reloads their loaders through the normal loading
protocol.

- Type: `(opts?: {filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean, sync?: boolean, forcePending?: boolean }) => Promise<void>`
- This is useful any time your loader data might be out of date or stale. For example, if you have a route that displays a list of posts, and you have a loader function that fetches the list of posts from an API, you might want to invalidate the route matches for that route any time a new post is created so that the list of posts is always up-to-date.
- if `filter` is not supplied, all matches will be invalidated
- if `filter` is supplied, only matches for which `filter` returns `true` will be invalidated.
- if `sync` is true, the promise returned by this function will only resolve once all loaders have finished.
- if `forcePending` is true, the invalidated matches will be put into `'pending'` state regardless whether they are in `'error'` state or not.
- If `filter` is not supplied, all committed and cached match generations are invalidated.
- If `filter` is supplied, it is evaluated against committed and cached matches. Selecting one generation invalidates every committed or cached generation with the same match ID.
- Invalidation reruns `beforeLoad`; reusable loader data is marked stale and reloads through the normal loading protocol. Route-level `context` remains reusable while the match ID is unchanged.
- If `sync` is `true`, stale loader work is blocking and the returned promise resolves after it finishes instead of leaving a background refresh detached.
- If `forcePending` is `true`, selected routes that need loading enter the normal pending protocol even when successful data was already available.
- You might also want to invalidate the Router if you imperatively `reset` the router's `CatchBoundary` to trigger loaders again.

### `.clearCache` method

Remove cached route matches.
Remove cached route matches and matching active preloads.

- Type: `(opts?: {filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean}) => void`
- if `filter` is not supplied, all cached matches will be removed
- if `filter` is supplied, only matches for which `filter` returns `true` will be removed.
- If `filter` is not supplied, all cached matches and active preload lanes are removed.
- If `filter` is supplied, matching cached matches are removed. An active preload lane is canceled when any match in that lane passes the filter.
- Current committed and presented matches are not removed.

### `.load` method

Expand All @@ -170,16 +164,25 @@ Loads all of the currently matched route matches and resolves when they are all

Preloads all of the matches that match the provided `NavigateOptions`.

> ⚠️⚠️⚠️ **Preloaded route matches are not stored long-term in the router state. They are only stored until the next attempted navigation action.**
An active preload is speculative and is not published as the current match
presentation. Successful loader data can enter the normal in-memory route cache
and remain reusable according to `preloadStaleTime` and `preloadGcTime`.

Completed preload `beforeLoad` context is not cached. A later navigation reruns
`beforeLoad` unless it adopts an identical whole-route preload that is still
active. Adoption also requires unchanged router context, additional context,
and user-supplied location state.

- Type: `(opts?: NavigateOptions) => Promise<RouteMatch[]>`
- Type: `(opts: NavigateOptions) => Promise<RouteMatch[] | undefined>`
- Properties
- `opts`
- Type: `NavigateOptions`
- Optional, defaults to the current location.
- Required.
- The options that will be used to determine which route matches to preload.
- Returns
- A promise that resolves with an array of all of the route matches that were preloaded.
- A promise that resolves with the speculative route-match lane. An ordinary error or not-found is represented by a terminal match array rather than a rejected promise.
- It resolves with `undefined` when cancellation or control flow does not produce a reusable lane.
- The method is also available on server router instances. It remains speculative and does not change the request's current location or presented matches.

### `.loadRouteChunk` method

Expand Down
5 changes: 3 additions & 2 deletions docs/router/api/router/useChildMatchesHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ title: useChildMatches hook

The `useChildMatches` hook returns all of the child [`RouteMatch`](./RouteMatchType.md) objects from the closest match down to the leaf-most match. **It does not include the current match, which can be obtained using the `useMatch` hook.**

> [!IMPORTANT]
> If the router has pending matches and they are showing their pending component fallbacks, pending matches are used instead of active matches.
The result is derived from the router's current match presentation. It can
include still-loading descendants and descendants below the current render
boundary.

## useChildMatches options

Expand Down
2 changes: 1 addition & 1 deletion docs/router/api/router/useMatchesHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: useMatchesHook
title: useMatches hook
---

The `useMatches` hook returns all of the [`RouteMatch`](./RouteMatchType.md) objects from the router **regardless of its callers position in the React component tree**.
The `useMatches` hook returns the router's complete presented array of [`RouteMatch`](./RouteMatchType.md) objects **regardless of its caller's position in the component tree**. The array can include still-loading descendants or matches below a pending/error/not-found render boundary.

> [!TIP]
> If you only want the parent or child matches, then you can use the [`useParentMatches`](./useParentMatchesHook.md) or the [`useChildMatches`](./useChildMatchesHook.md) based on the selection you need.
Expand Down
Loading
Loading