首页
/ 在 Preact Query 中接入 GraphQL:类型安全查询、代码生成与实战落地

在 Preact Query 中接入 GraphQL:类型安全查询、代码生成与实战落地

2026-09-08 18:31:05作者:魏侃纯Zoe

本文面向使用 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-queryReact 替换为 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 导出了 useQueryuseQueries 等 hook,同时第 4 行 export * from '@tanstack/query-core' 表明所有框架适配层都建立在 query-core 之上;
  • queryFn 的返回类型与执行都围绕 Promise 展开,框架本身不关心请求协议是 REST、GraphQL 还是 gRPC。

因此,"Preact Query 是否支持 GraphQL"不是一个需要插件或适配器的问题——它是架构上天生支持的。你需要的只是:

  1. 一个 QueryClient 实例并挂载 QueryClientProvider
  2. 一个返回 Promise 的 queryFn(内部调用任意 GraphQL 客户端);
  3. 一个稳定的 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,其 peerDependenciespreact: ^10.0.0,也就是说要求 Preact 10 及以上;
  • graphql-request 主流的 v5+ 版本支持 TS 泛型推断,适合与类型安全代码生成配合;
  • graphql 是 GraphQL 解析与类型系统基础库,graphql-request 依赖它来解析查询文档。

仓库中对应的运行示例依赖(examples/react/basic-graphql-request/package.json)使用了 graphql-request@^7.1.2graphql@^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)同样会被类型检查

思路分两步:

  1. GraphQL Code Generator 根据 schema 生成类型化的 graphql() 辅助函数(通常输出到 gql/gql.ts);
  2. 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 类型;
  • useQueryqueryFn 只需返回 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,useQuerydata 就是强类型的——类型系统由请求库一路贯通到组件渲染层。

把 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 即可运行)。该示例演示了几个值得借鉴的工程模式:

  1. 列表/详情两套查询共用缓存:用 usePosts()(key 为 ['posts'])与 usePost(postId)(key 为 ['post', postId])区分两个查询;
  2. 用缓存判断数据是否已存在:列表页通过 queryClient.getQueryData(['post', post.id]) 判断详情是否已被缓存,据此把已访问过的文章标题加粗显示——这是非规范化缓存模型下"预取感知"的典型用法;
  3. 后台刷新提示isFetching 为 true 时显示 "Background Updating...",让用户感知重新访问时发生的数据刷新;
  4. 条件启用:详情查询使用 enabled: !!postId示例源码),postId 无效时不发起请求。

该示例同样使用 graphql-requestgql + 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 可以把同一组选项在 useQueryqueryClient.prefetchQuery 等命令式 API 之间复用,详情可参考 query-options 指南

与取消、重试等能力的协同

GraphQL 请求同样享受 Preact Query 的完整生命周期能力:

  • 查询重试、超时由 query-core 统一管理;
  • 若使用支持 AbortSignal 的 GraphQL 客户端,还可配合 query-cancellation 指南 实现请求取消;
  • 失效与后台刷新逻辑对 GraphQL 与 REST 一视同仁,可参阅 query-functions 指南 了解 queryFn 的上下文参数(如 signalqueryKeymeta)。

小结

把本文要点收敛为一张接入决策清单:

  1. 架构前提:Preact Query 基于 Promise 抽象,GraphQL 客户端天然可用,无需任何适配层;
  2. 边界认知:无规范化缓存,多查询共享实体需通过统一的 queryKey + 失效/写入策略管理;
  3. 类型安全路线graphql-request v5+ 提供泛型推断;叠加 GraphQL Code Generator 生成的 graphql(),可让查询文档、variables、返回数据三者类型闭环;
  4. 运行骨架QueryClient + QueryClientProvider + 返回 Promise 的 queryFn + 稳定 queryKey,即可让 GraphQL 应用获得缓存、后台刷新、重试、失效等开箱能力;
  5. 仓库参照React 版 GraphQL 示例 展示了含缓存感知 UI、后台刷新提示的完整列表/详情模式,迁移到 Preact 仅需替换包名。

以 Promise 为桥,Preact Query 与 GraphQL 的结合是"配置即所得"的简单加法——把精力留给真正需要决策的缓存策略与类型设计即可。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391