Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions docs/router/api/router/RouterStateType.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ If you previously used `router.state.pendingMatches`, use one of these patterns
- `router.state.isLoading` to detect active loading work.
- `router.matchRoute(...)` with `{ pending: true }` when you need to match against the pending location.

If you previously used `router.state.cachedMatches`, note that cached matches are now internal router state and are no longer exposed on `RouterState`.

### `status` property

- Type: `'pending' | 'idle'`
Expand Down
1 change: 1 addition & 0 deletions docs/router/api/router/RouterType.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Invalidates route matches by forcing their `beforeLoad` and `load` functions to
Remove cached route matches.

- Type: `(opts?: {filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean}) => void`
- Cached matches are stored internally and are no longer exposed on `router.state`.
- 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.

Expand Down
2 changes: 2 additions & 0 deletions docs/router/api/router/useRouterStateHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The `useRouterState` method is a hook that returns the current internal state of

> [!TIP]
> If you want to access the current location or the current matches, you should try out the [`useLocation`](./useLocationHook.md) and [`useMatches`](./useMatchesHook.md) hooks first. These hooks are designed to be more ergonomic and easier to use than accessing the router state directly.
>
> Cached route matches are internal router state and are not exposed through `useRouterState`.

## useRouterState options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBe(4)
expect(updates).toBe(1)
})

test('sync beforeLoad', async () => {
Expand Down Expand Up @@ -225,8 +225,8 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBeGreaterThanOrEqual(9)
expect(updates).toBeLessThanOrEqual(11)
expect(updates).toBeGreaterThanOrEqual(5)
expect(updates).toBeLessThanOrEqual(7)
})

test('navigate, w/ preloaded & async loaders', async () => {
Expand Down Expand Up @@ -293,6 +293,6 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBe(1)
expect(updates).toBe(0)
})
})
124 changes: 55 additions & 69 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,6 @@ export interface RouterState<
isLoading: boolean
isTransitioning: boolean
matches: Array<TRouteMatch>
cachedMatches: Array<TRouteMatch>
location: ParsedLocation<FullSearchSchema<TRouteTree>>
resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
statusCode: number
Expand Down Expand Up @@ -963,6 +962,7 @@ export class RouterCore<
latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
pendingBuiltLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
private pendingMatchesInternal?: Array<AnyRouteMatch>
private cachedMatchesInternal: Array<AnyRouteMatch> = []
basepath!: string
routeTree!: TRouteTree
routesById!: RoutesById<TRouteTree>
Expand Down Expand Up @@ -1017,6 +1017,25 @@ export class RouterCore<
return !!this.options.isPrerendering
}

private setCachedMatchesInternal = (matches: Array<AnyRouteMatch>) => {
this.cachedMatchesInternal = matches.filter(
(match) => match.status !== 'redirected',
)
}

private updateCachedMatchesInternal = (
updater: (matches: Array<AnyRouteMatch>) => Array<AnyRouteMatch>,
) => {
this.setCachedMatchesInternal(updater(this.cachedMatchesInternal))
}

private appendCachedMatchesInternal = (matches: Array<AnyRouteMatch>) => {
if (!matches.length) {
return
}
this.setCachedMatchesInternal([...this.cachedMatchesInternal, ...matches])
}

update: UpdateFn<
TRouteTree,
TTrailingSlashOption,
Expand Down Expand Up @@ -1117,16 +1136,7 @@ export class RouterCore<
getInitialRouterState(this.latestLocation),
) as unknown as Store<any>
} else {
this.__store = new Store(getInitialRouterState(this.latestLocation), {
onUpdate: () => {
this.__store.state = {
...this.state,
cachedMatches: this.state.cachedMatches.filter(
(d) => !['redirected'].includes(d.status),
),
}
},
})
this.__store = new Store(getInitialRouterState(this.latestLocation))

setupScrollRestoration(this)
}
Expand Down Expand Up @@ -2342,6 +2352,11 @@ export class RouterCore<
// Match the routes
const pendingMatches = this.matchRoutes(this.latestLocation)
this.pendingMatchesInternal = pendingMatches
const pendingMatchIds = new Set(pendingMatches.map((match) => match.id))

this.updateCachedMatchesInternal((matches) =>
matches.filter((match) => !pendingMatchIds.has(match.id)),
)

// Ingest the new matches
this.__store.setState((s) => ({
Expand All @@ -2350,10 +2365,6 @@ export class RouterCore<
statusCode: 200,
isLoading: true,
location: this.latestLocation,
// If a cached moved to pendingMatches, remove it from cachedMatches
cachedMatches: s.cachedMatches.filter(
(d) => !pendingMatches.some((e) => e.id === d.id),
),
}))
}

Expand Down Expand Up @@ -2429,21 +2440,15 @@ export class RouterCore<
isLoading: false,
loadedAt: Date.now(),
matches: newMatches,
/**
* When committing new matches, cache any exiting matches that are still usable.
* Routes that resolved with `status: 'error'` or `status: 'notFound'` are
* deliberately excluded from `cachedMatches` so that subsequent invalidations
* or reloads re-run their loaders instead of reusing the failed/not-found data.
*/
cachedMatches: [
...s.cachedMatches,
...exitingMatches.filter(
(d) =>
d.status !== 'error' && d.status !== 'notFound',
),
],
}
})
this.appendCachedMatchesInternal(
exitingMatches.filter(
(match) =>
match.status !== 'error' &&
match.status !== 'notFound',
),
)
this.pendingMatchesInternal = undefined
this.clearExpiredCache()
})
Expand Down Expand Up @@ -2593,27 +2598,26 @@ export class RouterCore<
return
}

const matchesKey = this.state.matches.some((d) => d.id === id)
? 'matches'
: this.state.cachedMatches.some((d) => d.id === id)
? 'cachedMatches'
: ''

if (matchesKey) {
if (this.state.matches.some((d) => d.id === id)) {
this.__store.setState((s) => ({
...s,
[matchesKey]: s[matchesKey]?.map((d) =>
d.id === id ? updater(d) : d,
),
matches: s.matches.map((d) => (d.id === id ? updater(d) : d)),
}))
return
}

if (this.cachedMatchesInternal.some((d) => d.id === id)) {
this.updateCachedMatchesInternal((matches) =>
matches.map((d) => (d.id === id ? updater(d) : d)),
)
}
})
}

getMatch: GetMatchFn = (matchId: string): AnyRouteMatch | undefined => {
const findFn = (d: { id: string }) => d.id === matchId
return (
this.state.cachedMatches.find(findFn) ??
this.cachedMatchesInternal.find(findFn) ??
this.pendingMatchesInternal?.find(findFn) ??
this.state.matches.find(findFn)
)
Expand All @@ -2636,8 +2640,8 @@ export class RouterCore<
TDehydrated
>
> = (opts) => {
const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
const invalidate = <TMatch extends AnyRouteMatch>(d: TMatch): TMatch => {
if (opts?.filter?.(d as unknown as MakeRouteMatchUnion<this>) ?? true) {
return {
...d,
invalid: true,
Expand All @@ -2646,17 +2650,17 @@ export class RouterCore<
d.status === 'notFound'
? ({ status: 'pending', error: undefined } as const)
: undefined),
}
} as TMatch
}
return d
}

this.pendingMatchesInternal = this.pendingMatchesInternal?.map(invalidate)
this.updateCachedMatchesInternal((matches) => matches.map(invalidate))

this.__store.setState((s) => ({
...s,
matches: s.matches.map(invalidate),
cachedMatches: s.cachedMatches.map(invalidate),
}))

this.shouldViewTransition = false
Expand Down Expand Up @@ -2712,21 +2716,11 @@ export class RouterCore<
clearCache: ClearCacheFn<this> = (opts) => {
const filter = opts?.filter
if (filter !== undefined) {
this.__store.setState((s) => {
return {
...s,
cachedMatches: s.cachedMatches.filter(
(m) => !filter(m as MakeRouteMatchUnion<this>),
),
}
})
this.updateCachedMatchesInternal((matches) =>
matches.filter((m) => !filter(m as MakeRouteMatchUnion<this>)),
)
} else {
this.__store.setState((s) => {
return {
...s,
cachedMatches: [],
}
})
this.setCachedMatchesInternal([])
}
}

Expand Down Expand Up @@ -2780,20 +2774,13 @@ export class RouterCore<

const loadedMatchIds = new Set([
...activeMatchIds,
...this.state.cachedMatches.map((d) => d.id),
...this.cachedMatchesInternal.map((d) => d.id),
])

// If the matches are already loaded, we need to add them to the cachedMatches
batch(() => {
matches.forEach((match) => {
if (!loadedMatchIds.has(match.id)) {
this.__store.setState((s) => ({
...s,
cachedMatches: [...(s.cachedMatches as any), match],
}))
}
})
})
this.appendCachedMatchesInternal(
matches.filter((match) => !loadedMatchIds.has(match.id)),
)

try {
matches = await loadMatches({
Expand Down Expand Up @@ -2941,7 +2928,6 @@ export function getInitialRouterState(
resolvedLocation: undefined,
location,
matches: [],
cachedMatches: [],
statusCode: 200,
}
}
Expand Down
20 changes: 10 additions & 10 deletions packages/router-core/tests/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ describe('beforeLoad skip or exec', () => {
const beforeLoad = vi.fn()
const router = setup({ beforeLoad })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
expect(router.getMatch('/foo/foo')).toEqual(
expect.objectContaining({ id: '/foo/foo' }),
)
await sleep(10)
await router.navigate({ to: '/foo' })
Expand All @@ -121,8 +121,8 @@ describe('beforeLoad skip or exec', () => {
const router = setup({ beforeLoad })
router.preloadRoute({ to: '/foo' })
await Promise.resolve()
expect(router.state.cachedMatches).toEqual(
expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
expect(router.getMatch('/foo/foo')).toEqual(
expect.objectContaining({ id: '/foo/foo' }),
)
await router.navigate({ to: '/foo' })

Expand Down Expand Up @@ -290,8 +290,8 @@ describe('loader skip or exec', () => {
const loader = vi.fn()
const router = setup({ loader })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
expect(router.getMatch('/foo/foo')).toEqual(
expect.objectContaining({ id: '/foo/foo' }),
)
await sleep(10)
await router.navigate({ to: '/foo' })
Expand All @@ -303,8 +303,8 @@ describe('loader skip or exec', () => {
const loader = vi.fn()
const router = setup({ loader, staleTime: 1000 })
await router.preloadRoute({ to: '/foo' })
expect(router.state.cachedMatches).toEqual(
expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
expect(router.getMatch('/foo/foo')).toEqual(
expect.objectContaining({ id: '/foo/foo' }),
)
await sleep(10)
await router.navigate({ to: '/foo' })
Expand All @@ -317,8 +317,8 @@ describe('loader skip or exec', () => {
const router = setup({ loader })
router.preloadRoute({ to: '/foo' })
await Promise.resolve()
expect(router.state.cachedMatches).toEqual(
expect.arrayContaining([expect.objectContaining({ id: '/foo/foo' })]),
expect(router.getMatch('/foo/foo')).toEqual(
expect.objectContaining({ id: '/foo/foo' }),
)
await router.navigate({ to: '/foo' })

Expand Down
Loading
Loading