154 lines
4.1 KiB
Markdown
154 lines
4.1 KiB
Markdown
# load-ensure-query-data: Use ensureQueryData with TanStack Query
|
|
|
|
## Priority: HIGH
|
|
|
|
## Explanation
|
|
|
|
When integrating TanStack Router with TanStack Query, use `queryClient.ensureQueryData()` in loaders instead of `prefetchQuery()`. This respects the cache, awaits data if missing, and returns the data for potential use.
|
|
|
|
## Bad Example
|
|
|
|
```tsx
|
|
// Using prefetchQuery - doesn't return data, can't await stale check
|
|
export const Route = createFileRoute('/posts/$postId')({
|
|
loader: async ({ params, context: { queryClient } }) => {
|
|
// prefetchQuery never throws, swallows errors
|
|
queryClient.prefetchQuery({
|
|
queryKey: ['posts', params.postId],
|
|
queryFn: () => fetchPost(params.postId),
|
|
})
|
|
// No await - might not complete before render
|
|
// No return value to use
|
|
},
|
|
})
|
|
|
|
// Fetching directly - bypasses TanStack Query cache
|
|
export const Route = createFileRoute('/posts')({
|
|
loader: async () => {
|
|
const posts = await fetchPosts() // Not cached
|
|
return { posts }
|
|
},
|
|
})
|
|
```
|
|
|
|
## Good Example
|
|
|
|
```tsx
|
|
// Define queryOptions for reuse
|
|
const postQueryOptions = (postId: string) =>
|
|
queryOptions({
|
|
queryKey: ['posts', postId],
|
|
queryFn: () => fetchPost(postId),
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
})
|
|
|
|
export const Route = createFileRoute('/posts/$postId')({
|
|
loader: async ({ params, context: { queryClient } }) => {
|
|
// ensureQueryData:
|
|
// - Returns cached data if fresh
|
|
// - Fetches and caches if missing or stale
|
|
// - Awaits completion
|
|
// - Throws on error (caught by error boundary)
|
|
await queryClient.ensureQueryData(postQueryOptions(params.postId))
|
|
},
|
|
component: PostPage,
|
|
})
|
|
|
|
function PostPage() {
|
|
const { postId } = Route.useParams()
|
|
|
|
// Data guaranteed to exist from loader
|
|
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
|
|
|
|
return <PostContent post={post} />
|
|
}
|
|
```
|
|
|
|
## Good Example: Multiple Parallel Queries
|
|
|
|
```tsx
|
|
export const Route = createFileRoute('/dashboard')({
|
|
loader: async ({ context: { queryClient } }) => {
|
|
// Parallel data fetching
|
|
await Promise.all([
|
|
queryClient.ensureQueryData(statsQueries.overview()),
|
|
queryClient.ensureQueryData(activityQueries.recent()),
|
|
queryClient.ensureQueryData(notificationQueries.unread()),
|
|
])
|
|
},
|
|
})
|
|
```
|
|
|
|
## Good Example: Dependent Queries
|
|
|
|
```tsx
|
|
export const Route = createFileRoute('/users/$userId/posts')({
|
|
loader: async ({ params, context: { queryClient } }) => {
|
|
// First query needed for second
|
|
const user = await queryClient.ensureQueryData(
|
|
userQueries.detail(params.userId)
|
|
)
|
|
|
|
// Dependent query uses result
|
|
await queryClient.ensureQueryData(
|
|
postQueries.byAuthor(user.id)
|
|
)
|
|
},
|
|
})
|
|
```
|
|
|
|
## Router Configuration for TanStack Query
|
|
|
|
```tsx
|
|
// router.tsx
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 60 * 1000, // 1 minute default
|
|
},
|
|
},
|
|
})
|
|
|
|
export const router = createRouter({
|
|
routeTree,
|
|
context: { queryClient },
|
|
|
|
// Let TanStack Query manage caching
|
|
defaultPreloadStaleTime: 0,
|
|
|
|
// SSR: Dehydrate query cache
|
|
dehydrate: () => ({
|
|
queryClientState: dehydrate(queryClient),
|
|
}),
|
|
|
|
// SSR: Hydrate on client
|
|
hydrate: (dehydrated) => {
|
|
hydrate(queryClient, dehydrated.queryClientState)
|
|
},
|
|
|
|
// Wrap with QueryClientProvider
|
|
Wrap: ({ children }) => (
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
),
|
|
})
|
|
```
|
|
|
|
## ensureQueryData vs prefetchQuery vs fetchQuery
|
|
|
|
| Method | Returns | Throws | Awaits | Use Case |
|
|
|--------|---------|--------|--------|----------|
|
|
| `ensureQueryData` | Data | Yes | Yes | Route loaders (recommended) |
|
|
| `prefetchQuery` | void | No | Yes | Background prefetching |
|
|
| `fetchQuery` | Data | Yes | Yes | When you need data immediately |
|
|
|
|
## Context
|
|
|
|
- `ensureQueryData` is the recommended method for route loaders
|
|
- Respects `staleTime` - won't refetch fresh cached data
|
|
- Errors propagate to route error boundaries
|
|
- Use `queryOptions()` factory for type-safe, reusable query definitions
|
|
- Set `defaultPreloadStaleTime: 0` to let TanStack Query manage cache
|
|
- Pair with `useSuspenseQuery` in components for guaranteed data
|