在 Preact Query 中接入 GraphQL:类型安全查询、代码生成与实战落地
本文面向使用 TanStack Query 构建 Preact 应用的开发者,讲解如何将 GraphQL 数据层无缝接入 @tanstack/preact-query。核心要点是:Preact Query 的抓取机制以 Promise 为抽象基石,因此可以与 graphql-request、Apollo Client 等任意异步数据请求库协同工作;在此基础上,再结合 GraphQL Code Generator 实现全链路类型安全的查询与代码生成。读完本文,你将掌握 GraphQL + Preact Query 的完整接入模式,并能在当前仓库中找到可直接运行的参考示例与底层源码依据。
为什么 Preact Query 能与 GraphQL 顺畅协作
在 TanStack Query 的框架文档中,Preact 版 GraphQL 指南与 React 版共享同一份内容源——docs/framework/preact/graphql.md 的 front-matter 中声明了 ref: docs/framework/react/graphql.md,并通过 replace 规则将 react-query 替换为 preact-query、React 替换为 Preact。这也从文档工程层面印证了一个核心事实:数据抓取机制对数据源完全无感。
原文明确指出:
Because React Query's fetching mechanisms are agnostically built on Promises, you can use React Query with literally any asynchronous data fetching client, including GraphQL!
换成 Preact Query 语境同样成立:只要你的 queryFn 返回一个 Promise,Preact Query 就能接管其状态管理与缓存。GraphQL 客户端(graphql-request、Apollo、urql 等)本质上都是"把 GraphQL 请求变成 Promise"的库,因此天然适配。
从仓库源码可以进一步印证这一设计:
- packages/preact-query/src/index.ts 导出了
useQuery、useQueries等 hook,同时第 4 行export * from '@tanstack/query-core'表明所有框架适配层都建立在query-core之上; queryFn的返回类型与执行都围绕 Promise 展开,框架本身不关心请求协议是 REST、GraphQL 还是 gRPC。
因此,"Preact Query 是否支持 GraphQL"不是一个需要插件或适配器的问题——它是架构上天生支持的。你需要的只是:
- 一个 QueryClient 实例并挂载
QueryClientProvider; - 一个返回 Promise 的
queryFn(内部调用任意 GraphQL 客户端); - 一个稳定的
queryKey。
必须知道的边界:没有规范化缓存(normalized cache)
原文在开篇用一个醒目提示强调了一个重要的边界:
Keep in mind that React Query does not support normalized caching. While a vast majority of users do not actually need a normalized cache or even benefit from it as much as they believe they do, there may be very rare circumstances that may warrant it so be sure to check with us first to make sure it's truly something you need!
翻译并展开说明:
- TanStack Query(含 Preact Query)不做规范化缓存。缓存的最小单位是"查询键(queryKey)+ 查询函数返回的数据",数据以
queryKey为索引整体存放,而不会像 Apollo/Relay 那样把实体按类型与 ID 拆散、再自动组合。 - 对绝大多数应用而言,这并非缺陷:基于 queryKey 的"结构共享 + 精确失效"模式更简单、更可预测,配合
queryClient.setQueryData/ 失效机制 已能覆盖绝大多数场景。 - 但如果你确实需要依赖"同一实体多处自动同步更新"的能力,即多个查询自动共享同一份实体数据且任一修改全局生效,那么规范化缓存才值得考虑。原文的建议是:先确认自己是否真的需要,不要默认引入。
从实现层面看,query-core 中的数据存储围绕 Query(由 queryKey 标识)展开,缓存中不存在跨查询的实体索引结构,这正是"不提供规范化缓存"的底层原因。
准备工作:安装依赖
要让 Preact Query + GraphQL 跑起来,需要安装:
npm install @tanstack/preact-query
npm install graphql graphql-request
版本与依赖约束说明:
- packages/preact-query/package.json 显示当前仓库中该包版本为
5.102.8,其peerDependencies为preact: ^10.0.0,也就是说要求 Preact 10 及以上; graphql-request主流的 v5+ 版本支持 TS 泛型推断,适合与类型安全代码生成配合;graphql是 GraphQL 解析与类型系统基础库,graphql-request依赖它来解析查询文档。
仓库中对应的运行示例依赖(examples/react/basic-graphql-request/package.json)使用了 graphql-request@^7.1.2、graphql@^16.9.0 与 @tanstack/react-query@^5.102.8。Preact 场景下仅需将 query 包替换为 @tanstack/preact-query。
类型安全与代码生成:graphql-request v5 + GraphQL Code Generator
原文给出的进阶玩法是:Preact Query + graphql-request(v5 及以上)+ GraphQL Code Generator,以获得对 GraphQL operation 的完整类型检查——不仅查询结果的 data 是强类型的,查询变量(variables)同样会被类型检查。
思路分两步:
- 用 GraphQL Code Generator 根据 schema 生成类型化的
graphql()辅助函数(通常输出到gql/gql.ts); - 在
queryFn中把graphql(...)生成的类型化查询文档交给graphql-request执行。
以"查询星球大战系列电影列表"为例,原文的示例落到 Preact Query 语境如下:
import { request } from 'graphql-request'
import { useQuery } from '@tanstack/preact-query'
import { graphql } from './gql/gql'
const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `
query allFilmsWithVariablesQuery($first: Int!) {
allFilms(first: $first) {
edges {
node {
id
title
}
}
}
}
`)
function Films() {
// `data` 是完全类型化的!
const { data } = useQuery({
queryKey: ['films'],
queryFn: async () =>
request(
'https://swapi-graphql.netlify.app/.netlify/functions/index',
allFilmsWithVariablesQueryDocument,
// variables 同样会被类型检查!
{ first: 10 },
),
})
// ...
}
代码要点拆解:
./gql/gql是 Code Generator 生成的类型模块,graphql()函数会把字符串模板解析为类型安全的 GraphQL 文档对象;request(endpoint, document, variables)三个参数全部参与类型推导:查询文档决定了 variables 与返回值的 TS 类型;useQuery的queryFn只需返回 Promise——request的结果天然满足这一约束;queryKey: ['films']是缓存的唯一标识,跨组件复用该 key 即可共享缓存。
类型参数推导链
结合 docs/framework/preact/reference/functions/useQuery.md 中记录的函数签名:
function useQuery<TQueryFnData, TError, TData, TQueryKey>(options, queryClient?): DefinedUseQueryResult<TData, TError>
queryFn 返回什么类型,TQueryFnData 就会被推断为什么类型,进而 data 字段自动获得相同类型。所以只要 GraphQL 客户端返回强类型 Promise,useQuery 的 data 就是强类型的——类型系统由请求库一路贯通到组件渲染层。
把 GraphQL + Preact Query 的最小应用完整跑起来
为了让上文代码真正可运行,还需要补上 QueryClient 的引导(bootstrap)部分。一个最小可运行的 Preact 应用骨架如下:
import { render } from 'preact'
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/preact-query'
import { request } from 'graphql-request'
const endpoint = 'https://graphqlzero.almansi.me/api'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Posts />
</QueryClientProvider>
)
}
function Posts() {
const { status, data, error, isFetching } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { posts } = await request<{ posts: Array<{ id: number; title: string }> }>(
endpoint,
/* GraphQL */ `
query {
posts {
id
title
}
}
`,
)
return posts
},
})
if (status === 'pending') return <p>Loading...</p>
if (status === 'error') return <p>Error: {error.message}</p>
return (
<div>
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
{isFetching ? <p>Background Updating...</p> : null}
</div>
)
}
render(<App />, document.getElementById('root')!)
其中:
QueryClientProvider负责向组件树提供 queryClient 上下文,所有useQuery调用都会从最近的 Provider 取用实例;- 没有显式泛型时,
data的类型由request<T>的泛型决定,也可在useQuery上显式声明四个类型参数; status/isFetching等返回值与 React Query 完全一致,渲染分支模式通用。
仓库内的可运行参考示例
当前仓库虽然没有独立的 Preact GraphQL 示例目录,但在 examples/react/basic-graphql-request/src/index.tsx 中提供了一个结构完整、可直接对照学习的示例(源码中已配置 Vite,npm install && npm run dev 即可运行)。该示例演示了几个值得借鉴的工程模式:
- 列表/详情两套查询共用缓存:用
usePosts()(key 为['posts'])与usePost(postId)(key 为['post', postId])区分两个查询; - 用缓存判断数据是否已存在:列表页通过
queryClient.getQueryData(['post', post.id])判断详情是否已被缓存,据此把已访问过的文章标题加粗显示——这是非规范化缓存模型下"预取感知"的典型用法; - 后台刷新提示:
isFetching为 true 时显示 "Background Updating...",让用户感知重新访问时发生的数据刷新; - 条件启用:详情查询使用
enabled: !!postId(示例源码),postId 无效时不发起请求。
该示例同样使用 graphql-request(gql + request 具名导入)访问 GraphQLZero 的公开 GraphQL API(https://graphqlzero.almansi.me/api)。把其中的 @tanstack/react-query 导入替换为 @tanstack/preact-query、组件写法改为 Preact 即可迁移到 Preact 场景——这与本文开头提到的文档 replace 映射逻辑完全一致。
进阶:封装自定义 GraphQL hooks,沉淀可复用数据层
实践中通常会把"端点 + 文档 + queryKey"封装为自定义 hook,避免在组件里散落 GraphQL 细节。例如对文章详情场景:
function usePost(postId: number) {
return useQuery({
queryKey: ['post', postId],
queryFn: async () => {
const { post } = await request<{ post: Post }>(
endpoint,
/* GraphQL */ `
query {
post(id: ${postId}) {
id
title
body
}
}
`,
)
return post
},
enabled: !!postId,
})
}
这套写法的收益:
- 所有 GraphQL 请求细节(文档字符串、端点、类型)都被封装在 hook 内,组件只消费
data/status/error; - 配合 Code Generator 时,hook 的入参
postId与返回值都自动获得精确类型; - 结合
queryOptions可以把同一组选项在useQuery与queryClient.prefetchQuery等命令式 API 之间复用,详情可参考 query-options 指南。
与取消、重试等能力的协同
GraphQL 请求同样享受 Preact Query 的完整生命周期能力:
- 查询重试、超时由
query-core统一管理; - 若使用支持 AbortSignal 的 GraphQL 客户端,还可配合 query-cancellation 指南 实现请求取消;
- 失效与后台刷新逻辑对 GraphQL 与 REST 一视同仁,可参阅 query-functions 指南 了解
queryFn的上下文参数(如signal、queryKey、meta)。
小结
把本文要点收敛为一张接入决策清单:
- 架构前提:Preact Query 基于 Promise 抽象,GraphQL 客户端天然可用,无需任何适配层;
- 边界认知:无规范化缓存,多查询共享实体需通过统一的 queryKey + 失效/写入策略管理;
- 类型安全路线:
graphql-requestv5+ 提供泛型推断;叠加 GraphQL Code Generator 生成的graphql(),可让查询文档、variables、返回数据三者类型闭环; - 运行骨架:
QueryClient+QueryClientProvider+ 返回 Promise 的queryFn+ 稳定queryKey,即可让 GraphQL 应用获得缓存、后台刷新、重试、失效等开箱能力; - 仓库参照:React 版 GraphQL 示例 展示了含缓存感知 UI、后台刷新提示的完整列表/详情模式,迁移到 Preact 仅需替换包名。
以 Promise 为桥,Preact Query 与 GraphQL 的结合是"配置即所得"的简单加法——把精力留给真正需要决策的缓存策略与类型设计即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00