关于 router-query
面向熟悉 TanStack Router 与 TanStack Query 的开发者,说明如何将 Query 用作外部数据缓存并与路由加载器协作。涵盖客户端数据获取、SSR 水合与流式查询,并指出缓存协调中的关键配置。
主要功能
- 01协调路由加载器与 Query 缓存
- 02静态查询与组件订阅模式
- 03非阻塞后台查询与流式处理
- 04SSR 查询状态水合与传输
- 05错误处理及常见配置说明
适用场景
- 01在导航期间预取并缓存页面数据
- 02为非关键数据启用后台加载
- 03为 SSR 应用配置查询水合
- 04避免路由缓存干扰数据新鲜度
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill router-query安装后在 Claude Code 中输入/router-query确认已加载。
详细介绍
TanStack Router + TanStack Query 集成
此技能要求熟悉 TanStack Router 和 TanStack Query。请先阅读 @tanstack/router-core#router-core 和 react-router。
此技能介绍如何将 TanStack Query 用作外部数据缓存,并与 TanStack Router 的加载器系统协调。路由器充当协调器——它在导航期间触发数据获取,而 Query 负责缓存、后台重新获取和数据生命周期。
CRITICAL:使用 TanStack Query 时,请设置
defaultPreloadStaleTime: 0。否则,Router 内置的预加载缓存(默认 30s)会阻止 Query 控制数据的新鲜度。
CRITICAL:对于 SSR,请在
createRouter工厂函数内部创建QueryClient。在模块级别创建单例会导致不同服务器请求之间的数据泄漏。
设置:Router 上下文中的 QueryClient
基础(仅客户端)
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
RouterProvider,
createRouter,
createRootRouteWithContext,
} from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
// Root route declares that router context includes queryClient
// (root route file creates it with createRootRouteWithContext — see below)
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0, // Let Query manage caching
context: { queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
return <RouterProvider router={router} />
}
带上下文的根路由
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
// Double parentheses: factory pattern
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
component: () => <Outlet />,
})
SSR 安全设置
// src/router.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
// Fresh QueryClient per request — prevents data leaking between SSR requests
const queryClient = new QueryClient()
return createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createAppRouter>
}
}
使用 setupRouterSsrQueryIntegration 进行 SSR
自动进行 SSR 脱水/水合和流式传输:
npm install @tanstack/react-router-ssr-query
// src/router.tsx
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
})
setupRouterSsrQueryIntegration({
router,
queryClient,
// wrapQueryClient: true (default — wraps with QueryClientProvider)
// handleRedirects: true (default — handles redirect() from queries)
})
return router
}
此集成会:
- 在服务器上对查询状态进行脱水,并在客户端自动进行水合
- 将服务器渲染期间完成的查询流式传输到客户端
- 处理从查询/变更中抛出的
redirect()
手动 SSR 脱水/水合(不使用 SSR Query 包)
// src/router.tsx
import { QueryClient, dehydrate, hydrate } from '@tanstack/react-query'
import { QueryClientProvider } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
const queryClient = new QueryClient()
return createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
dehydrate: () => ({
queryClientState: dehydrate(queryClient),
}),
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
核心模式:在加载器中使用静态 query,在组件中使用 useSuspenseQuery
这是推荐的模式。加载器会在渲染前查询数据(不会出现加载闪烁),并使用 staleTime: 'static' 复用缓存数据,而不触发过期数据的重新获取。组件会订阅缓存以接收更新。
// src/routes/posts.tsx
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
interface Post {
id: string
title: string
}
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: (): Promise<Array<Post>> =>
fetch('/api/posts').then((r) => r.json()),
})
export const Route = createFileRoute('/posts')({
loader: ({ context }) => {
// query returns cached data when available and fetches when it is missing
// Keep this query static so a stale refetch does not block navigation
return context.queryClient.query({
...postsQueryOptions,
staleTime: 'static',
})
},
component: PostsPage,
})
function PostsPage() {
// useSuspenseQuery subscribes to cache — gets background updates
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
使用动态参数
// src/routes/posts/$postId.tsx
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
interface Post {
id: string
title: string
content: string
}
const postQueryOptions = (postId: string) =>
queryOptions({
queryKey: ['posts', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()),
})
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context, params }) => {
return context.queryClient.query({
...postQueryOptions(params.postId),
staleTime: 'static',
})
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
return <article>{post.title}</article>
}
流式传输模式:触发但不等待 query
对于非关键数据,启动查询但不阻塞导航。使用 noop 处理其拒绝状态,避免后台失败成为未处理的拒绝:
import { noop, useQuery, useSuspenseQuery } from '@tanstack/react-query'
export const Route = createFileRoute('/dashboard')({
loader: ({ context }) => {
// Await critical data
const user = context.queryClient.query({
...userQueryOptions,
staleTime: 'static',
})
// Start non-critical query without awaiting — streams during SSR
void context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
return user
},
component: Dashboard,
})
function Dashboard() {
// Critical: suspense (data ready immediately)
const { data: user } = useSuspenseQuery(userQueryOptions)
// Non-critical: regular query (shows loading state)
const { data: analytics, isLoading } = useQuery(analyticsQueryOptions)
return (
<div>
<h1>Welcome {user.name}</h1>
{isLoading ? <Skeleton /> : <AnalyticsChart data={analytics} />}
</div>
)
}
使用 useQueryErrorResetBoundary 处理错误
import { useEffect } from 'react'
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: ({ context }) =>
context.queryClient.query({
...postsQueryOptions,
staleTime: 'static',
}),
errorComponent: PostsErrorComponent,
component: PostsPage,
})
function PostsErrorComponent({
error,
reset,
}: {
error: unknown
reset: () => void
}) {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
<p>{error instanceof Error ? error.message : String(error)}</p>
<button onClick={() => router.invalidate()}>Retry</button>
</div>
)
}
常见错误
1. HIGH:未将 defaultPreloadStaleTime 设置为 0
Router 内置了预加载缓存(预加载的默认 staleTime 为 30s)。这会阻止 Query 在预加载期间控制数据的新鲜度。
// WRONG — Router's preload cache serves stale data, Query never refetches
const router = createRouter({ routeTree })
// CORRECT — disable Router's preload cache, let Query manage freshness
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
2. HIGH:在 SSR 中于 createRouter 外部创建 QueryClient
模块级单例 QueryClient 会由所有服务器请求共享,导致不同请求之间的用户数据泄漏。
// WRONG — shared across SSR requests
const queryClient = new QueryClient()
export function createAppRouter() {
return createRouter({
routeTree,
context: { queryClient },
})
}
// CORRECT — new QueryClient per createAppRouter call
export function createAppRouter() {
const queryClient = new QueryClient()
return createRouter({
routeTree,
context: { queryClient },
})
}
3. MEDIUM:在加载器中等待即发即弃的查询会阻塞渲染
用于流式传输的查询应在后台启动,并使用 catch(noop) 处理错误。等待该查询会阻塞导航转换,直到数据解析完成,从而违背流式传输的目的。
import { noop } from '@tanstack/react-query'
// WRONG — blocks navigation, no streaming benefit
loader: async ({ context }) => {
await context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// CORRECT — fire and forget for streaming, with handled rejection
loader: ({ context }) => {
void context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// If you need to block (critical data), use a static query instead:
loader: ({ context }) => {
return context.queryClient.query({
...criticalQueryOptions,
staleTime: 'static',
})
}
4. HIGH:createRootRouteWithContext 缺少双括号
createRootRouteWithContext<Type>() 是一个工厂函数——它会返回一个函数。第二次调用时传入路由选项。
// WRONG — passing options to the factory, not the returned function
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({
component: RootComponent,
})
// CORRECT — double call: factory()({options})
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
component: RootComponent,
})
权衡:内置 SWR 缓存与外部缓存
TanStack Router 有自己的 SWR 缓存(staleTime、gcTime、defaultPreloadStaleTime)。将 Query 用作外部缓存时:
- 将
defaultPreloadStaleTime: 0,防止 Router 缓存绕过 Query 的新鲜度逻辑 - Router 的
staleTime/gcTime仍会应用于加载器的返回值。对于纯 Query 模式,加载器不返回任何内容(只使用静态query执行副作用),并且只通过useSuspenseQuery读取数据 router.invalidate()会重新运行加载器(加载器会调用staleTime: 'static'的query),因此缓存的 Query 数据仍是新鲜度判断的依据
交叉引用
@tanstack/router-core#router-core/data-loading— 内置加载器缓存基础知识@tanstack/router-core#router-core/ssr— 用于脱水/水合的 SSR 设置