Skip to content

Commit

Permalink
fix: preloading and potentially fix devtools context
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 15, 2023
1 parent 2f04260 commit 5ed8e42
Show file tree
Hide file tree
Showing 8 changed files with 1,703 additions and 682 deletions.
1,958 changes: 1,431 additions & 527 deletions docs/api/api.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/api/router-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ tsr watch

TSR CLI's generator is designed with an exceptional degree of flexibility, capable of accommodating both flat and nested file structures, or even a combination of the two without any additional configuration. In a flat structure, all files reside at the same level, with specific prefixes and suffixes denoting their relationships and behaviors. Conversely, a nested structure organizes related files into directories, using nesting to illustrate relationships. Both approaches are valuable, so the generator automatically supports mixed configurations where flat and nested structures coexist within the same route tree. This adaptability ensures that developers have complete control over their route organization, allowing for the customization of the routing structure to align seamlessly with various project needs and preferences.

Both of the following configurations will result in a route path of `/posts/:postId/edit`:
Both of the following configurations will result in a route path of `/posts/$postId/edit`:

- Flat Syntax: `posts.$postId.edit.tsx`
- Nested Syntax:
Expand Down
73 changes: 57 additions & 16 deletions docs/guide/preloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,96 @@
title: Preloading
---

Preloading in TanStack Router is a way to load a route before the user actually navigates to it. This is useful for routes that are likely to be visited by the user, but not necessarily on the first page load.
Preloading in TanStack Router is a way to load a route before the user actually navigates to it. This is useful for routes that are likely to be visited by the user next. For example, if you have a list of posts and the user is likely to click on one of them, you can preload the post route so that it's ready to go when the user clicks on it.

**Preloading works by using hover and touch start events on `<Link>` components to preload the dependencies for the destination route.**
## Supported Preloading Strategies

The simplest way to preload routes for your application is to set the `preload` option to `intent` for your entire router:
- Intent
- **Preloading by "intent" works by using hover and touch start events on `<Link>` components to preload the dependencies for the destination route.**
- Render
- **Coming soon!**
- Viewport Visiblity
- **Coming soon!**

## How long does preloaded data stay in memory?

Preloaded route matches are temporarily cached in memory with a few important caveats:

- **Unused preloaded data is removed after 30 seconds by default.** This can be configured by setting the `defaultPreloadMaxAge` option on your router.
- **Obviously, when a a route is loaded, its preloaded version is promoted to the router's normal pending matches state.**

If you need more control over preloading, caching and/or garbage collection of preloaded data, you should use an external caching library like [TanStack Query](https://react-query.tanstack.com)

The simplest way to preload routes for your application is to set the `defaultPreload` option to `intent` for your entire router:

```tsx
import { Router } from '@tanstack/react-router'

const router = new Router({
// ...
preload: 'intent',
defaultPreload: 'intent',
})
```

This will turn on preloading by default for all `<Link>` components in your application. You can also set the `preload` prop on individual `<Link>` components to override the default behavior.
This will turn on `intent` preloading by default for all `<Link>` components in your application. You can also set the `preload` prop on individual `<Link>` components to override the default behavior.

## Preload Delay

By default, preloading will start after **50ms** of the user hovering or touching a `<Link>` component. You can change this delay by setting the `preloadDelay` option on your router:
By default, preloading will start after **50ms** of the user hovering or touching a `<Link>` component. You can change this delay by setting the `defaultPreloadDelay` option on your router:

```tsx
import { Router } from '@tanstack/react-router'

const router = new Router({
// ...
preloadDelay: 100,
defaultPreloadDelay: 100,
})
```

You can also set the `preloadDelay` prop on individual `<Link>` components to override the default behavior on a per-link basis.

## Preloading with Data Loaders
## Preloading supports Data Loaders and External Libraries

Preloading supports both built-in loaders and your favorite data loading libraries! To use the built-in loaders, simply return data from your route's loader function and optionally configure the `shouldReload` option to control when the route is preloaded.

If you'd rather use your favorite data loading library, you'll likely want to keep the aggressive default behavior of triggering preload logic every time the a `<Linkuser hovers or touches>` component, then allow your external library to control the actual data loading and caching strategy.

## Build-in Preloading & `shouldReload`

If you're using the built-in loaders, you can control when a route is preloaded by setting the `shouldReload` option on the route.

Preloading is most useful when combined with the built-in loaders (or your favorite data loading library). To make this easier, the `loader` route option function receives a `preload` boolean denoting whether the route is being preloaded or not. **If you're using the built-in loader state, preload states are already handled for you automatically**. If that's not the case, then the `preload` flag allows you to load data differently depending on whether the user is navigating to the route or preloading it.
The `shouldReload` option on a route will be respected for preloading in the exact same way it is respected for normal route loading:

Here's a simple example of how you might use the `preload` flag with the built-in loader state:
- If `shouldReload` is `true` (the default), the route will always be preloaded when the user triggers the preload. This might be a bit aggressive, but it's a thorough default to ensure that the route is always up-to-date.
- If `shouldReload` is `false`, the route will only be preloaded if the route is not already preloaded. This is useful for routes that are not likely to change often.
- If `shouldReload` is a function and returns `true`/`false`, the route will be preloaded based on the return value of the function.
- If `shouldReload` is a function and returns a dependency object/array, the route will be preloaded if the dependency object has changed since the last time the route was preloaded.

### Example

```tsx
import { Route } from '@tanstack/react-router'
import { Router } from '@tanstack/react-router'

const postsRoute = new Route({
getParentRoute: () => rootRoute,
path: 'posts',
loader: ({ preload }) => fetchPosts({ isPreload: preload }),
const postRoute = new Route({
path: '/posts/$id',
loader: async ({ params }) => fetchPost(params.id),
// Preload the route if the cache is older than 10 seconds
shouldReload: ({ params }) => Math.floor(Date.now() / 10_000),
})

const router = new Router({
// ...
defaultPreload: 'intent',
})
```

Not all data loading libraries will differentiate between preloading or not, but usually a preload method or option is available and can control different aspects of the actual data fetching or caching strategy of the data.
## Preloading with External Libraries

It's common for external caching libraries to have their own tracking mechanisms for when data is stale. For example, [React Query](https://react-query.tanstack.com) has a `staleTime` option that controls how long data is considered fresh.

Since this is the norm, the default behavior of preloading in TanStack Router is to always trigger the preload logic when the user hovers or touches a `<Link>` component, then allow your external library to control the actual data loading and caching strategy.

Simply put **if you're using an external data loading library, you probably don't need to configure the `shouldReload` option**.

## Preloading Manually

Expand Down
3 changes: 2 additions & 1 deletion examples/react/basic/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const postRoute = new Route({
getParentRoute: () => postsRoute,
path: '$postId',
errorComponent: PostErrorComponent,
shouldReload: ({ cause }) => cause === 'enter',
shouldReload: false, // Will only load on match for first preload or on 'enter'
loader: ({ params }) => fetchPost(params.postId),
component: PostComponent,
})
Expand Down Expand Up @@ -191,6 +191,7 @@ const routeTree = rootRoute.addChildren([
const router = new Router({
routeTree,
notFoundRoute,
defaultPreload: 'intent',
})

// Register things for typesafety
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export interface RouteMatch<
context: RouteById<TRouteTree, TRouteId>['types']['allContext']
search: FullSearchSchema<TRouteTree> &
RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
fetchedAt: number
fetchCount: number
shouldReloadDeps: any
abortController: AbortController
cause: 'enter' | 'stay'
cause: 'preload' | 'enter' | 'stay'
}

export type AnyRouteMatch = RouteMatch<any>
Expand Down
16 changes: 11 additions & 5 deletions packages/react-router/src/RouterProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ export type BuildLocationFn<TRouteTree extends AnyRoute> = (

export type InjectedHtmlEntry = string | (() => Promise<string> | string)

export const routerContext = React.createContext<Router<any>>(null!)
export let routerContext = React.createContext<Router<any>>(null!)

if (typeof document !== 'undefined') {
window.__TSR_ROUTER_CONTEXT__ = routerContext as any
if (window.__TSR_ROUTER_CONTEXT__) {
routerContext = window.__TSR_ROUTER_CONTEXT__
} else {
window.__TSR_ROUTER_CONTEXT__ = routerContext as any
}
}

export function RouterProvider<
Expand Down Expand Up @@ -212,9 +216,11 @@ export function getRouteMatch<TRouteTree extends AnyRoute>(
state: RouterState<TRouteTree>,
id: string,
): undefined | RouteMatch<TRouteTree> {
return [...(state.pendingMatches ?? []), ...state.matches].find(
(d) => d.id === id,
)
return [
...state.preloadMatches,
...(state.pendingMatches ?? []),
...state.matches,
].find((d) => d.id === id)
}

export function useRouterState<
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ type BeforeLoadFn<
location: ParsedLocation
navigate: NavigateFn<AnyRoute>
buildLocation: BuildLocationFn<AnyRoute>
cause: 'enter' | 'stay'
cause: 'preload' | 'enter' | 'stay'
}) => Promise<TRouteContext> | TRouteContext | void

export type UpdatableRouteOptions<
Expand Down Expand Up @@ -268,7 +268,7 @@ export interface LoaderFnContext<
location: ParsedLocation<TFullSearchSchema>
navigate: (opts: NavigateOptions<AnyRoute>) => Promise<void>
parentMatchPromise?: Promise<void>
cause: 'enter' | 'stay'
cause: 'preload' | 'enter' | 'stay'
}

export type SearchFilter<T, U = T> = (prev: T) => U
Expand Down
Loading

0 comments on commit 5ed8e42

Please sign in to comment.