---
id: loader-query-cache
track: learn
product: query
locale: zh-CN
order: 5
revision: 2
sourceCheckedOn: "2026-08-03"
versionRange: "Query ^5 / Router ^1"
verifiedAgainst: "@tanstack/react-query@5.101.4 + @tanstack/react-router@1.170.18 + @tanstack/react-router-ssr-query@1.167.1"
contentModel: 2
prerequisites:
  - "理解 Loader 与 Router context"
  - "已安装 React Query SSR 集成"
sourceRefs:
  - "https://tanstack.com/router/latest/docs/integrations/query"
---

# 让 Loader 预热 Query 缓存

> Router 决定何时导航，Query 决定数据如何缓存；用同一份 queryOptions 把两者接起来。

**Outcome:** 能在 Loader 中预取关键数据，并在组件中读取同一个缓存条目。

## 只定义一次查询契约

queryOptions 把 queryKey 与 queryFn 放在一起，并保留类型推断。Loader 和组件必须引用同一个契约，避免同一资源被两个不相关的 key 重复请求。

File: `src/features/issues/queries.ts`

```ts
import { queryOptions } from '@tanstack/react-query'

export const issuesQuery = queryOptions({
  queryKey: ['issues'],
  queryFn: () => getIssues(),
  staleTime: 30_000,
})
```

## 导航前填充，渲染时读取

ensureQueryData 会在缓存可用时复用它，否则等待请求完成。useSuspenseQuery 随后读取同一个条目；SSR 集成负责把服务端缓存脱水并在客户端恢复。

> **Note:** SSR 环境要为每个请求创建新的 QueryClient，不能在模块顶层共享。

File: `src/routes/issues/index.tsx`

```tsx
import { useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { issuesQuery } from '~/features/issues/queries'

export const Route = createFileRoute('/issues/')({
  loader: ({ context }) =>
    context.queryClient.ensureQueryData(issuesQuery),
  component: IssuesPage,
})

function IssuesPage() {
  const { data } = useSuspenseQuery(issuesQuery)
  return <p>{data.length} issues</p>
}
```

## 实践与验证

### 完成检查点

路由 Loader 使用同一份 queryOptions 预取数据，组件读取时不会再创建第二套 Query Key。

### 动手练习

为 Issue 详情建立 queryOptions 工厂，并在详情路由 Loader 中通过 issueId 预热缓存。

### 验证方法

首次导航只应出现一次详情请求；返回后再次进入时应遵循配置的新鲜度策略。

### 常见错误

- Loader 与组件使用结构不同的 Query Key。
- 在 Loader 中等待不影响首屏的独立请求。

## Official sources

- https://tanstack.com/router/latest/docs/integrations/query
