Skip to content

Commit

Permalink
fix: preloading without long-term caching
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 19, 2023
1 parent 9c05914 commit dd91643
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 30 deletions.
14 changes: 11 additions & 3 deletions docs/guide/data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ To control router dependencies and "freshness", TanStack Router provides a pleth

### Using `loaderDeps` to access search params

Imagine a `/posts` route supports some pagination via search params `offset` and `limit`. For the cache to uniquely store this data, we need to access these search params via the `loaderDeps` function. By explicityly identifying them, each route match for `/posts` with different offset and limit won't get mixed up!
Imagine a `/posts` route supports some pagination via search params `offset` and `limit`. For the cache to uniquely store this data, we need to access these search params via the `loaderDeps` function. By explicitly identifying them, each route match for `/posts` with different offset and limit won't get mixed up!

Once we have these deps in place, the route will always reload when the deps change.

Expand Down Expand Up @@ -179,21 +179,29 @@ const router = new Router({
})
```

## Using `shouldReload` to opt-out of loader reloading
## Using `shouldReload` and `gcTime` to opt-out of caching

Similar to Remix's defaulf functionality, you may want to configure a route to only load on entry or when critical loader deps change. You can do this by using the `shouldReload` option, which accepts either a `boolean` or a function that receives the same `beforeLoad` and `loaderContext` parameters and returns a boolean indicating if the route should reload.
Similar to Remix's default functionality, you may want to configure a route to only load on entry or when critical loader deps change. You can do this by using the `gcTime` option combined with the `shouldReload` option, which accepts either a `boolean` or a function that receives the same `beforeLoad` and `loaderContext` parameters and returns a boolean indicating if the route should reload.

```tsx
const postsRoute = new Route({
getParentPath: () => rootRoute,
path: 'posts',
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// Do not cache this route's data after it's unloaded
gcTime: 0,
// Only reload the route when the user navigates to it or when deps change
shouldReload: false,
})
```

### Opting out of caching while still preloading

Even though you may opt-out of short-term caching for your route data, you can still get the benefits of preloading! With the above configuration, preloading will still "just work" with the default `preloadGcTime`. This means that if a route is preloaded, then navigated to, the route's data will be considered fresh and will not be reloaded.

If you'd like to opt out of preloading as well, simply don't turn it on via the `routerOptions.defaultPreload` or `routeOptions.preload` options.

## Passing through all loader events to an external cache

We break down this use case in the [External Data Loading](./external-data-loading) page, but if you'd like to use an external cache like TanStack Query, you can do so by passing through all loader events to your external cache. As long as you are using the defaults, the only change you'll need to make is to set the `defaultPreloadStaleTime` option on the router to `0`:
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface RouteMatch<
abortController: AbortController
cause: 'preload' | 'enter' | 'stay'
loaderDeps: RouteById<TRouteTree, TRouteId>['types']['loaderDeps']
preload: boolean
invalid: boolean
}

Expand Down
74 changes: 47 additions & 27 deletions packages/react-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ export class Router<
cause,
loaderDeps,
invalid: false,
preload: false,
}

// Regardless of whether we're reusing an existing match or creating
Expand Down Expand Up @@ -1257,6 +1258,12 @@ export class Router<
? shouldReloadOption(loaderContext)
: shouldReloadOption

matches[index] = match = {
...match,
preload:
!!preload && !this.state.matches.find((d) => d.id === match.id),
}

if (match.status !== 'success') {
// If we need to potentially show the pending component,
// start a timer to show it after the pendingMs
Expand Down Expand Up @@ -1328,23 +1335,7 @@ export class Router<
const previousMatches = this.state.matches

this.__store.batch(() => {
// This is where all of the garbage collection magic happens
this.__store.setState((s) => {
return {
...s,
cachedMatches: s.cachedMatches.filter((d) => {
const route = this.looseRoutesById[d.routeId]!

return (
d.status !== 'error' &&
Date.now() - d.updatedAt <
(route.options.gcTime ??
this.options.defaultGcTime ??
5 * 60 * 1000)
)
}),
}
})
this.cleanCache()

// Match the routes
pendingMatches = this.matchRoutes(next.pathname, next.search, {
Expand Down Expand Up @@ -1393,16 +1384,19 @@ export class Router<

// Commit the pending matches. If a previous match was
// removed, place it in the cachedMatches
this.__store.setState((s) => ({
...s,
isLoading: false,
matches: pendingMatches,
pendingMatches: undefined,
cachedMatches: [
...s.cachedMatches,
...exitingMatches.filter((d) => d.status !== 'error'),
],
}))
this.__store.batch(() => {
this.__store.setState((s) => ({
...s,
isLoading: false,
matches: s.pendingMatches!,
pendingMatches: undefined,
cachedMatches: [
...s.cachedMatches,
...exitingMatches.filter((d) => d.status !== 'error'),
],
}))
this.cleanCache()
})

//
;(
Expand Down Expand Up @@ -1440,6 +1434,32 @@ export class Router<
return this.latestLoadPromise
}

cleanCache = () => {
// This is where all of the garbage collection magic happens
this.__store.setState((s) => {
return {
...s,
cachedMatches: s.cachedMatches.filter((d) => {
const route = this.looseRoutesById[d.routeId]!

if (!route.options.loader) {
return false
}

// If the route was preloaded, use the preloadGcTime
// otherwise, use the gcTime
const gcTime =
(d.preload
? route.options.preloadGcTime ?? this.options.defaultPreloadGcTime
: route.options.gcTime ?? this.options.defaultGcTime) ??
5 * 60 * 1000

return d.status !== 'error' && Date.now() - d.updatedAt < gcTime
}),
}
})
}

preloadRoute = async (
navigateOpts: ToOptions<TRouteTree> = this.state.location as any,
) => {
Expand Down

0 comments on commit dd91643

Please sign in to comment.