Skip to content

Commit

Permalink
feat: loaderDeps
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 15, 2023
1 parent 3cbff96 commit 8e7645a
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 100 deletions.
39 changes: 5 additions & 34 deletions docs/api/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1164,12 +1164,13 @@ type shouldReload =
- If a function is provided that returns a `boolean`, the above rules will be applied based on the return value of the function.
- If a function is provided that returns an **object or array** of dependencies, the route will be reloaded when any of the dependencies change. Changes are tracked using deep equality checks.
#### `key`
#### `loaderDeps`
- Type: `(opts: { search: TFullSearchSchema; location: ParsedLocation }) => any`
- Type: `(opts: { search: TFullSearchSchema; location: ParsedLocation, context: TAllContext }) => Record<string, any>`
- Optional
- A function that will be called before this route is matched to provide additional unique identification to the route match. It should return any serializable value that can uniquely identify the route match from navigation to navigation.
- If your route match relies on a search params for unique identification, it's recommended to use the `key` option to return a unique value based on the search params. This will ensure that the route match is not shared between locations that have different search params.
- 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.
- 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 or context values for unique identification, it's required that you return them here so they can be made available in the `loader`'s `deps` argument.
#### `caseSensitive`
Expand Down Expand Up @@ -1564,36 +1565,6 @@ export type ParseRoute<TRouteTree extends AnyRoute> =
This type recursively parses a route's children and grandchildren into a single union of all possible routes.
```tsx
export type ParseRouteChildren<TRouteTree extends AnyRoute> =
TRouteTree extends Route<
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
infer TChildren,
any
>
? unknown extends TChildren
? never
: TChildren extends AnyRoute[]
? {
[TId in TChildren[number]['id'] as string]: ParseRoute<
TChildren[number]
>
}[string]
: never
: never
```
# `RoutesById` type
This type takes a route tree and returns a Record of all routes in the tree keyed by their route ID.
Expand Down
30 changes: 18 additions & 12 deletions docs/guide/data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,27 +103,25 @@ const postsRoute = new Route({

By passing `false` to the `shouldReload` option, we are telling the router to never reload the `/posts` route after the initial `enter` lifecycle. This means that if the user navigates to `/posts` from `/about`, the `loader` function will be called. If the user then navigates to `/posts/$postId`, the `loader` function will not be called.

### Using a function that returns dependencies to opt-out of `loader` calls
### Using `loaderDeps` and `shouldReload` together

Imagine our `/posts` route supports some pagination via search params `offset` and `limit`. We may only want to reload the route when those search params change. We can do this by passing a function that returns an array of dependencies to the `shouldReload` option:
Imagine our `/posts` route supports some pagination via search params `offset` and `limit`. To access these search params, we'll need to use the `loaderDeps` function and pass them to our `loader` to uniquely identify each route match by the offset and the limit. Once we have these deps in place we know our route will always reload when the deps change, so we can opt-out of subsequent reloads with `shouldReload: false`.

```tsx
const postsRoute = new Route({
getParentPath: () => rootRoute,
path: 'posts',
loader: ({ search: { offset, limit } }) =>
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) =>
fetchPosts({
offset,
limit,
}),
shouldReload: ({ search: { offset, limit } }) => [offset, limit],
shouldReload: false,
})
```

In this example, the `loader` function will be called:

- On initial `enter` lifecycle
- On navigations when the `offset` or `limit` search params change
In this example, the `loader` function will **only** be called on the initial `enter` or `preload` actions for each unique offset and limit combination

### Achieving short-term Stale-While-Revalidate caching with `shouldReload`

Expand Down Expand Up @@ -227,11 +225,17 @@ const postsRoute = new Route({
})
```

## Using Search Params
## Using Search Params in Loaders

> ❓ But wait Tanner... where the heck are my search params?!
You might be here wondering why `search` isn't directly available in the `loader` function's parameters. We've purposefully designed it this way to help you succeed. Let's take a look at why:

> ⚠️ Using search params in loaders is likely indication that you should also be uniquely identifying your routes with a unique `key` option. This is because search params are not used in the default matchID that is used to uniquely identify route matches . This means that if you have two routes with the same path but different search params, they will be considered the same route and will not be reloaded when the search params change. This is usually not the desired behavior.
- Search Parameters being used in a loader function are a very good indicator that these search params should also be used to uniquely identify the data being loaded. For instance, the route match for page 1 of a list of posts is uniquely different than the route match for page 2 of a list of posts.
- Directly accessing search params in a loader function can lead to bugs where the data being loaded is not unique to the route match. For example, you might ask your `/posts` route to preload page 2's results, but because the route match is being stored under the `/posts` match ID, you would get page 2's data on your screen instead of it preloading in the background!
- Placing a threshold between search parameters and the loader function allows the router to understand your dependencies and reactivity.

Search parameters can be accessed via the `beforeLoad` and `loader` functions. The `search` property provided to these functions contains _all_ of the search params including parent search params. In this example, we'll use zod to validate and parse the search params for the `/posts` route that uses pagination, then use them in our `loader` function.
### Accessing Search Params via `routeOptions.loaderDeps`

```tsx
import { Route } from '@tanstack/react-router'
Expand All @@ -243,8 +247,10 @@ const postsRoute = new Route({
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// Pass the offset to your loader deps via the loaderDeps functino
loaderDeps: ({ search: { offset } }) => ({ offset }),
// Use the offset from context in the loader function
loader: async ({ search: { offset } }) =>
loader: async ({ deps: { offset } }) =>
fetchPosts({
offset,
}),
Expand Down
11 changes: 9 additions & 2 deletions docs/guide/route-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,20 @@ const router = new Router({

## Identifying Routes via Search Params

Search Params by default are not used to identify matching paths mostly because they are extremely flexible, flat and can contain a lot of unrelated data to your actual route definition. However, in some cases you may need to use them to uniquely identify a route match. For example, you may have a route that uses a search param to "key" the markup that is rendered for a route. Imagine a `/users/user` route that uses the search param `userId` to identify a specific user in your application, you might model your url like this: `/users/user?userId=123`. This means that your `user` route would need some extra help to identify a specific user. You can do this by adding a `key` function to your route:
Search Params by default are not used to identify matching paths mostly because they are extremely flexible, flat and can contain a lot of unrelated data to your actual route definition. However, in some cases you may need to use them to uniquely identify a route match. For example, you may have a route that uses a search param like `pageIndex` that uniquely identifies the data held inside of the route match. Or, imagine a `/users/user` route that uses the search param `userId` to identify a specific user in your application, you might model your url like this: `/users/user?userId=123`. This means that your `user` route would need some extra help to identify a specific user. Luckily, the only way to utilize search params in your route loaders is to provide them via a special `loaderDeps` route option. This option provides you all of the search params for the route match and allows you to return the ones you'll need inside of your loader.

```tsx
const userRoute = new Route({
getParentRoute: () => usersRoute,
validateSearch: (search) =>
search as {
userId: string
},
path: 'user',
key: ({ search }) => [search.userId],
loaderDeps: ({ search: { userId } }) => ({
userId,
}),
loader: async ({ params: { userId } }) => getUser(userId),
})
```

Expand Down
25 changes: 4 additions & 21 deletions docs/guide/search-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,28 +172,11 @@ The underlying mechanics why this works relies on the `validateSearch` function

Once your search params have been validated and typed, you're finally ready to start reading and writing to them. There are a few ways to do this in TanStack Router, so let's check them out.

### Search Params in Route Options
### Reading Search Params in Loaders

Thanks to TypeScript, you can access your route's validated search params in all sibling route options except `beforeLoad`:
Please read the [Search Params in Loaders](#search-params-in-loaders) section for more information about how to read search params in loaders with the `loaderDeps` option.

```tsx
const productSearchSchema = z.object({
page: z.number().catch(1),
filter: z.string().catch(''),
sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})

type ProductSearch = z.infer<typeof productSearchSchema>

const allProductsRoute = new Route({
getParentRoute: () => shopRoute,
path: 'products',
validateSearch: productSearchSchema,
loader: ({ search }) => {
// ^? ProductSearch ✅
},
})
```
### Search Params are inherited from Parent Routes

The search parameters and types of parents are merged as you go down the route tree, so child routes also have access to their parent's search params:

Expand All @@ -215,7 +198,7 @@ const allProductsRoute = new Route({
const productRoute = new Route({
getParentRoute: () => allProductsRoute,
path: ':productId',
loader: ({ search }) => {
beforeLoad: ({ search }) => {
search
// ^? ProductSearch ✅
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const Route = new FileRoute('/dashboard/users/user').createRoute({
validateSearch: z.object({
userId: z.number(),
}),
loader: ({ search: { userId } }) => fetchUserById(userId),
loaderDeps: ({ search: { userId } }) => ({ userId }),
loader: ({ deps: { userId } }) => fetchUserById(userId),
component: UserComponent,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ export const Route = new FileRoute('/dashboard/users/user').createRoute({
validateSearch: z.object({
userId: z.number(),
}),
key: ({ search }) => search.userId,
loaderDeps: ({ search: { userId } }) => ({ userId }),
loader: (opts) =>
opts.context.queryClient.ensureQueryData(
userQueryOptions(opts.search.userId),
userQueryOptions(opts.deps.userId),
),
component: UserComponent,
})
Expand Down
5 changes: 4 additions & 1 deletion examples/react/kitchen-sink-react-query/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,12 @@ const userRoute = new Route({
validateSearch: z.object({
userId: z.number(),
}),
loaderDeps: ({ search }) => ({
userId: search.userId,
}),
loader: (opts) =>
opts.context.queryClient.ensureQueryData(
userQueryOptions(opts.search.userId),
userQueryOptions(opts.deps.userId),
),
component: UserComponent,
})
Expand Down
6 changes: 5 additions & 1 deletion examples/react/kitchen-sink/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,10 @@ const userRoute = new Route({
validateSearch: z.object({
userId: z.number(),
}),
loader: ({ search: { userId } }) => fetchUserById(userId),
loaderDeps: ({ search: { userId } }) => ({
userId,
}),
loader: ({ deps: { userId } }) => fetchUserById(userId),
component: UserComponent,
})

Expand Down Expand Up @@ -803,6 +806,7 @@ const router = new Router({
context: {
auth: undefined!, // We'll inject this when we render
},
defaultPreload: 'intent',
})

declare module '@tanstack/react-router' {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export interface RouteMatch<
shouldReloadDeps: any
abortController: AbortController
cause: 'preload' | 'enter' | 'stay'
loaderDeps: RouteById<TRouteTree, TRouteId>['types']['loaderDeps']
}

export type AnyRouteMatch = RouteMatch<any>
export type AnyRouteMatch = RouteMatch<any, any>

export function Matches() {
const router = useRouter()
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/src/fileRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class FileRoute<
Assign<IsAny<TParentRoute['types']['allContext'], {}>, TRouteContext>
>,
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
TLoaderDeps extends Record<string, any> = {},
TLoaderData extends any = unknown,
TChildren extends RouteConstraints['TChildren'] = unknown,
TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
Expand All @@ -120,6 +121,7 @@ export class FileRoute<
TAllParams,
TRouteContext,
TContext,
TLoaderDeps,
TLoaderData
>,
'getParentRoute' | 'path' | 'id'
Expand All @@ -138,6 +140,7 @@ export class FileRoute<
TRouteContext,
TContext,
TRouterContext,
TLoaderDeps,
TLoaderData,
TChildren,
TRouteTree
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/src/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RegisteredRouter } from './router'

// Detect if we're in the DOM

export type AnyRedirect = Redirect<any, any, any>
export type AnyRedirect = Redirect<any, any, any, any, any>

export type Redirect<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
Expand Down

0 comments on commit 8e7645a

Please sign in to comment.