4.1 KiB
4.1 KiB
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
// 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
// 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
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
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
// 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
ensureQueryDatais 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: 0to let TanStack Query manage cache - Pair with
useSuspenseQueryin components for guaranteed data