diff --git a/docs/router/api/router/RouterStateType.md b/docs/router/api/router/RouterStateType.md index 65dd31db9a6..96db3c72c56 100644 --- a/docs/router/api/router/RouterStateType.md +++ b/docs/router/api/router/RouterStateType.md @@ -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'` diff --git a/docs/router/api/router/RouterType.md b/docs/router/api/router/RouterType.md index c5736638a0b..27c77f911b9 100644 --- a/docs/router/api/router/RouterType.md +++ b/docs/router/api/router/RouterType.md @@ -152,6 +152,7 @@ Invalidates route matches by forcing their `beforeLoad` and `load` functions to Remove cached route matches. - Type: `(opts?: {filter?: (d: MakeRouteMatchUnion) => 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. diff --git a/docs/router/api/router/useRouterStateHook.md b/docs/router/api/router/useRouterStateHook.md index bcded1d4158..5a17ce23231 100644 --- a/docs/router/api/router/useRouterStateHook.md +++ b/docs/router/api/router/useRouterStateHook.md @@ -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 diff --git a/packages/react-router/tests/store-updates-during-navigation.test.tsx b/packages/react-router/tests/store-updates-during-navigation.test.tsx index 69f60ceb609..e02c4e9eb2f 100644 --- a/packages/react-router/tests/store-updates-during-navigation.test.tsx +++ b/packages/react-router/tests/store-updates-during-navigation.test.tsx @@ -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 () => { @@ -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 () => { @@ -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) }) }) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index a8bbb6c8aa1..56290f4b966 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -531,7 +531,6 @@ export interface RouterState< isLoading: boolean isTransitioning: boolean matches: Array - cachedMatches: Array location: ParsedLocation> resolvedLocation?: ParsedLocation> statusCode: number @@ -963,6 +962,7 @@ export class RouterCore< latestLocation!: ParsedLocation> pendingBuiltLocation?: ParsedLocation> private pendingMatchesInternal?: Array + private cachedMatchesInternal: Array = [] basepath!: string routeTree!: TRouteTree routesById!: RoutesById @@ -1017,6 +1017,25 @@ export class RouterCore< return !!this.options.isPrerendering } + private setCachedMatchesInternal = (matches: Array) => { + this.cachedMatchesInternal = matches.filter( + (match) => match.status !== 'redirected', + ) + } + + private updateCachedMatchesInternal = ( + updater: (matches: Array) => Array, + ) => { + this.setCachedMatchesInternal(updater(this.cachedMatchesInternal)) + } + + private appendCachedMatchesInternal = (matches: Array) => { + if (!matches.length) { + return + } + this.setCachedMatchesInternal([...this.cachedMatchesInternal, ...matches]) + } + update: UpdateFn< TRouteTree, TTrailingSlashOption, @@ -1117,16 +1136,7 @@ export class RouterCore< getInitialRouterState(this.latestLocation), ) as unknown as Store } 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) } @@ -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) => ({ @@ -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), - ), })) } @@ -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() }) @@ -2593,19 +2598,18 @@ 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)), + ) } }) } @@ -2613,7 +2617,7 @@ export class RouterCore< 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) ) @@ -2636,8 +2640,8 @@ export class RouterCore< TDehydrated > > = (opts) => { - const invalidate = (d: MakeRouteMatch) => { - if (opts?.filter?.(d as MakeRouteMatchUnion) ?? true) { + const invalidate = (d: TMatch): TMatch => { + if (opts?.filter?.(d as unknown as MakeRouteMatchUnion) ?? true) { return { ...d, invalid: true, @@ -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 @@ -2712,21 +2716,11 @@ export class RouterCore< clearCache: ClearCacheFn = (opts) => { const filter = opts?.filter if (filter !== undefined) { - this.__store.setState((s) => { - return { - ...s, - cachedMatches: s.cachedMatches.filter( - (m) => !filter(m as MakeRouteMatchUnion), - ), - } - }) + this.updateCachedMatchesInternal((matches) => + matches.filter((m) => !filter(m as MakeRouteMatchUnion)), + ) } else { - this.__store.setState((s) => { - return { - ...s, - cachedMatches: [], - } - }) + this.setCachedMatchesInternal([]) } } @@ -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({ @@ -2941,7 +2928,6 @@ export function getInitialRouterState( resolvedLocation: undefined, location, matches: [], - cachedMatches: [], statusCode: 200, } } diff --git a/packages/router-core/tests/load.test.ts b/packages/router-core/tests/load.test.ts index eb2b1ce4761..4f26f246f5f 100644 --- a/packages/router-core/tests/load.test.ts +++ b/packages/router-core/tests/load.test.ts @@ -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' }) @@ -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' }) @@ -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' }) @@ -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' }) @@ -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' }) diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index ebdfdf15b04..ea8786ee15e 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -316,11 +316,7 @@ export const BaseTanStackRouterDevtoolsPanel = }) const activeMatch = createMemo(() => { - const matches = [ - ...pendingMatches(), - ...routerState().matches, - ...routerState().cachedMatches, - ] + const matches = [...pendingMatches(), ...routerState().matches] return matches.find( (d) => d.routeId === activeId() || d.id === activeId(), ) @@ -613,49 +609,6 @@ export const BaseTanStackRouterDevtoolsPanel = - {routerState().cachedMatches.length ? ( -
-
-
Cached Matches
-
- age / staleTime / gcTime -
-
-
- {routerState().cachedMatches.map((match: any) => { - return ( -
- setActiveId(activeId() === match.id ? '' : match.id) - } - class={cx(styles().matchRow(match === activeMatch()))} - > -
- - } - right={} - > - {`${match.id}`} - -
- ) - })} -
-
- ) : null}
{activeMatch() && activeMatch()?.status ? (
@@ -688,11 +641,7 @@ export const BaseTanStackRouterDevtoolsPanel = (d: any) => d.id === activeMatch()?.id, ) ? 'Pending' - : routerState().matches.find( - (d: any) => d.id === activeMatch()?.id, - ) - ? 'Active' - : 'Cached'} + : 'Active'}
diff --git a/packages/router-devtools-core/src/useStyles.tsx b/packages/router-devtools-core/src/useStyles.tsx index 5524ed0ae8f..129172ae0fe 100644 --- a/packages/router-devtools-core/src/useStyles.tsx +++ b/packages/router-devtools-core/src/useStyles.tsx @@ -399,11 +399,6 @@ const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { flex: 1 1 auto; overflow-y: auto; `, - cachedMatchesContainer: css` - flex: 1 1 auto; - overflow-y: auto; - max-height: 50%; - `, historyContainer: css` display: flex; flex: 1 1 auto; diff --git a/packages/solid-router/tests/store-updates-during-navigation.test.tsx b/packages/solid-router/tests/store-updates-during-navigation.test.tsx index d377346ce87..8a67b969a7d 100644 --- a/packages/solid-router/tests/store-updates-during-navigation.test.tsx +++ b/packages/solid-router/tests/store-updates-during-navigation.test.tsx @@ -156,7 +156,7 @@ describe("Store doesn't update *too many* times during navigation", () => { // that needs to be done during a navigation. // Any change that increases this number should be investigated. // Note: Solid has different update counts than React due to different reactivity - expect(updates).toBe(6) + expect(updates).toBe(2) }) test('sync beforeLoad', async () => { @@ -226,8 +226,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(8) - expect(updates).toBeLessThanOrEqual(10) + expect(updates).toBeGreaterThanOrEqual(3) + expect(updates).toBeLessThanOrEqual(5) }) test('navigate, w/ preloaded & async loaders', async () => { @@ -295,6 +295,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(2) + expect(updates).toBe(0) }) }) diff --git a/packages/vue-router/tests/store-updates-during-navigation.test.tsx b/packages/vue-router/tests/store-updates-during-navigation.test.tsx index f82b01799d4..21429c6f838 100644 --- a/packages/vue-router/tests/store-updates-during-navigation.test.tsx +++ b/packages/vue-router/tests/store-updates-during-navigation.test.tsx @@ -158,7 +158,7 @@ describe("Store doesn't update *too many* times during navigation", () => { // that needs to be done during a navigation. // Any change that increases this number should be investigated. // Note: Vue has different update counts than React/Solid due to different reactivity - expect(updates).toBe(10) + expect(updates).toBe(8) }) test('sync beforeLoad', async () => { @@ -234,8 +234,8 @@ describe("Store doesn't update *too many* times during navigation", () => { // that needs to be done during a navigation. // Any change that increases this number should be investigated. // Note: Vue has different update counts than React/Solid due to different reactivity - expect(updates).toBeGreaterThanOrEqual(33) - expect(updates).toBeLessThanOrEqual(35) + expect(updates).toBeGreaterThanOrEqual(30) + expect(updates).toBeLessThanOrEqual(32) }) test('navigate, w/ preloaded & async loaders', async () => {