news 2026/9/26 2:05:29

openapi-react-query:基于 TanStack Query 的 OpenAPI 全类型安全 React 数据请求方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
openapi-react-query:基于 TanStack Query 的 OpenAPI 全类型安全 React 数据请求方案
  • 开发工具
  • 代码生成
  • 后端

【免费下载链接】openapi-typescript

Generate TypeScript types from OpenAPI 3 specs

项目地址:https://gitcode.com/gh_mirrors/op/openapi-typescript
点击查看免费下载

openapi-react-query 是 openapi-typescript 开源生态中的 React 数据请求层,它把 @tanstack/react-query 的能力与 OpenAPI Schema 生成的 TypeScript 类型无缝衔接,为useQuery、useMutation、useSuspenseQuery、useInfiniteQuery等常用 Hook 提供 100% 类型安全的封装。本文将以 packages/openapi-react-query/README.md 为主线,结合官方文档与仓库源码,完整讲解安装配置、五大 API 的实战用法与底层实现原理,帮助你彻底告别手写 API 类型、消灭any与as类型断言。

什么是 openapi-react-query

openapi-react-query 是一个围绕 @tanstack/react-query 的类型安全微型封装(约 1 kb),专门用于配合 OpenAPI Schema 工作(见 README.md)。它本身不直接发送网络请求,而是依赖仓库中的另外两个核心包(见 docs/openapi-react-query/index.md):

  • openapi-fetch:负责实际发起 HTTP 请求的类型安全 fetch 客户端;
  • openapi-typescript:负责把 OpenAPI 3 规范(YAML/JSON)编译成 TypeScript 类型定义。

三者协作后,你可以获得以下开箱即用的能力(README.md):

  • ✅ URL 与参数零拼写错误:路径、查询参数全部由 Schema 类型约束;
  • ✅ 参数、请求体、响应体全部经过类型检查,与你的 Schema 100% 匹配;
  • ✅ 无需手动为 API 编写任何类型;
  • ✅ 消除掩盖 Bug 的any类型;
  • ✅ 消除同样可能掩盖 Bug 的as类型断言。

从 package.json 可以看到,该包体积设计极轻(README 声称约 1 kb),运行时仅依赖openapi-typescript-helpers提供类型工具,而@tanstack/react-query(^5.80.0)与openapi-fetch作为 peerDependencies 由使用方安装。

安装与初始化

安装依赖

按 README.md 的指引,同时安装运行时依赖与开发期依赖:

npm i openapi-react-query openapi-fetch npm i -D openapi-typescript typescript

其中:

包角色安装方式
openapi-react-query本文主角,React Query 类型安全封装dependencies
openapi-fetch底层请求客户端,被封装对象dependencies
openapi-typescript从 OpenAPI Schema 生成.d.ts类型devDependencies
typescript运行生成命令与类型检查devDependencies

从 Schema 生成类型

使用 openapi-typescript 的命令行工具,把 OpenAPI 文档(如v1.yaml)编译为 TypeScript 类型文件(README.md):

npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts

生成产物是一个包含paths等顶级类型的声明文件。例如仓库内的测试夹具 test/fixtures/api.d.ts 就是通过pnpm run generate-types(即openapi-typescript test/fixtures/api.yaml -o test/fixtures/api.d.ts,见 package.json)自动生成的。你可以在 packages/openapi-typescript/README.md 查看 openapi-typescript 的更多 CLI 选项(如--immutable、--export-type等)。

推荐:开启 noUncheckedIndexedAccess

官方文档强烈建议在tsconfig.json中开启 noUncheckedIndexedAccess,以及 docs/advanced.md 中的专项说明):

{ "compilerOptions": { "noUncheckedIndexedAccess": true } }

基础用法:三步发起第一个请求

第一步:创建 fetch 客户端

openapi-react-query 本身不接管网络层,它接收一个 openapi-fetch 创建的客户端实例。在项目的 API 模块(如src/api.ts)中:

import createFetchClient from "openapi-fetch"; import createClient from "openapi-react-query"; import type { paths } from "./my-openapi-3-schema"; // 由 openapi-typescript 生成 const fetchClient = createFetchClient<paths>({ baseUrl: "https://myapi.dev/v1/", }); export const $api = createClient(fetchClient);

这里有两层泛型传递(README.md):

  1. createFetchClient<paths>让 openapi-fetch 获得整份 Schema 的路径、方法与参数类型;
  2. createClient(fetchClient)从 fetch 客户端上推导出paths类型,返回一个类型完备的OpenapiQueryClient。

关于createFetchClient的更多细节(baseUrl、fetch自定义、中间件等),见 docs/openapi-fetch/index.md。

第二步:在组件中使用 $api.useQuery

const MyComponent = () => { const { data, error, isPending } = $api.useQuery( "get", "/blogposts/{post_id}", { params: { path: { post_id: 5 }, }, } ); if (isPending || !data) return "Loading..."; if (error) return `An error occurred: ${error.message}`; return <div>{data.title}</div>; };

这是 README.md 中的完整示例。注意三个关键点:

  • "get"与"/blogposts/{post_id}"都是字符串字面量类型:如果拼错 HTTP 方法或路径(例如 Schema 中不存在该路径),TypeScript 会直接报编译错误,从根本上杜绝 URL 拼写错误;
  • params.path.post_id被推断为number:传字符串或漏传都会报错;
  • data自动匹配 Schema 中该接口的 200 响应类型,error则匹配错误响应类型,二者完全类型化。

第三步:在 QueryClientProvider 中运行

与原生 @tanstack/react-query 一样,应用顶层需要QueryClientProvider(仓库测试在 test/index.test.tsx 中即如此包装):

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); export const App = () => ( <QueryClientProvider client={queryClient}> <MyComponent /> </QueryClientProvider> );

五大 API 详解

createClient返回的对象包含五个方法(对应 src/index.ts 中的OpenapiQueryClient接口):queryOptions、useQuery、useSuspenseQuery、useInfiniteQuery、useMutation。测试 test/index.test.tsx 明确断言了这五个方法的存在。

useQuery:标准数据查询

useQuery与 TanStack Query 原生useQuery行为一致,但额外具备(docs/openapi-react-query/use-query.md):

  • 返回值与原生useQuery完全相同;
  • 自动生成的 query key 为[method, path, params];
  • data与error完全类型化;
  • 支持第四个参数透传原生 query 选项。

完整签名(docs/openapi-react-query/use-query.md):

const query = $api.useQuery(method, path, options, queryOptions, queryClient);
参数必填说明
method✅HTTP 方法("get"等),参与 query key 生成
path✅Schema 中该方法可用的路径模板,参与 query key 生成
options视 Schema 而定fetch 选项;仅当 Schema 要求参数时必须提供,其params参与 query key 生成
queryOptions否原生useQuery的选项(enabled、select、initialData、refetchInterval等)
queryClient否自定义QueryClient实例(源码 src/index.ts 支持第五个可选参数)

为什么options有时必填有时可选?源码用RequiredKeysOf<Init> extends never条件类型判断:若 Schema 中该接口没有任何必填参数,则init与 options 均可省略;若存在必填参数(如路径变量post_id),则init必须提供(src/index.ts)。测试 test/index.test.tsx 验证了「Schema 要求 params 时缺参会报编译错误」。

useMutation:写操作

useMutation用于 POST/PUT/PATCH/DELETE 等写操作,其mutationKey为[method, path](docs/openapi-react-query/use-mutation.md)。典型示例——更新用户名字:

import { $api } from "./api"; export const App = () => { const { mutate } = $api.useMutation("patch", "/users"); return ( <button onClick={() => mutate({ body: { firstname: "John" } })}> Update </button> ); };

调用mutate(variables)时,variables(即 fetch 的 init 参数,含body、params等)同样被 Schema 严格约束——这里body.firstname必须匹配 Schema 中 PATCH/users的请求体定义。

签名(docs/openapi-react-query/use-mutation.md):

const mutation = $api.useMutation(method, path, queryOptions, queryClient);
  • method/path:必填,与useQuery相同,共同构成 mutationKey;
  • queryOptions:原生useMutation选项,如onMutate、onError、onSettled等;
  • queryClient:可选的自定义实例。

从源码看,mutation 的mutationFn把请求失败时的error直接throw出去,成功时返回data(并排除undefined,见 src/index.ts),因此onError、error状态的处理方式与原生完全一致。测试还验证了onMutate返回值的类型在onError/onSettled回调与context中保持一致(test/index.test.tsx),并同时支持mutate与mutateAsync两种调用方式。

useSuspenseQuery:Suspense 模式查询

如果你使用 React Suspense 渲染数据,useSuspenseQuery是首选(docs/openapi-react-query/use-suspense-query.md)。它的查询 key 同样是[method, path, params],data与error完全类型化,且函数签名与useQuery完全一致(docs/openapi-react-query/use-suspense-query.md)。

import { ErrorBoundary } from "react-error-boundary"; import { $api } from "./api"; const MyComponent = () => { const { data } = $api.useSuspenseQuery("get", "/users/{user_id}", { params: { path: { user_id: 5 }, }, }); return <div>{data.firstname}</div>; }; export const App = () => ( <ErrorBoundary fallbackRender={({ error }) => `Error: ${error.message}`}> <MyComponent /> </ErrorBoundary> );

Suspense 模式下组件内不再需要isPending/isLoading判断——数据未就绪时 React 会自动挂起,请求失败的错误则通过上层ErrorBoundary捕获(测试 test/index.test.tsx 用 500 响应验证了错误会正确抛给 Suspense/ErrorBoundary)。

useInfiniteQuery:无限滚动 / 分页

useInfiniteQuery在原生 API 之上额外内置了分页参数注入能力(docs/openapi-react-query/use-infinite-query.md)。典型的分页列表示例:

const PostList = () => { const { data, fetchNextPage, hasNextPage, isFetching } = $api.useInfiniteQuery( "get", "/posts", { params: { query: { limit: 10 }, }, }, { getNextPageParam: (lastPage) => lastPage.nextPage, initialPageParam: 0, } ); return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.items.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ))} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetching}> {isFetching ? "Loading..." : "Load More"} </button> )} </div> ); };

签名(docs/openapi-react-query/use-infinite-query.md):

const query = $api.useInfiniteQuery( method, path, options, infiniteQueryOptions, queryClient );

infiniteQueryOptions相比原生useInfiniteQuery选项,额外多出一个专属字段:

  • pageParamName(默认"cursor"):分页查询参数的名称。openapi-react-query 会在每次请求时自动把当前页游标注入到 URL query 中。

底层实现(src/index.ts)做了三件事:

  1. 解构出pageParamName(默认"cursor"),其余选项透传给原生useInfiniteQuery;
  2. queryFn中合并init参数,把[pageParamName]: pageParam写入params.query,同时透传signal用于请求取消;
  3. 首次请求pageParam默认为0。

测试对此有精确验证:请求/paginated-data?limit=3时,第一页自动带上cursor=0,fetchNextPage()后第二页自动带上cursor=1;将pageParamName改为"follow_cursor"后,查询参数随之变为follow_cursor=0/1(test/index.test.tsx)。测试同样覆盖了select重排 pages/pageParams 及自定义返回类型等场景。

queryOptions:与任意 Query API 组合

当需要的 API 不在上述五个方法中时(如useQueries批量查询、QueryClient.fetchQuery手动预取、usePrefetchQuery预取等),queryOptions是官方推荐的扩展口(docs/openapi-react-query/query-options.md)。它返回一个完全类型化的 Query Options 对象,其中:

  • queryKey为[method, path, params];
  • queryFn已内置为类型安全的 fetcher;
  • data/error会被正确推导(docs/openapi-react-query/query-options.md)。

配合原生useQuery使用:

import { useQuery } from '@tanstack/react-query'; import { $api } from "./api"; export const App = () => { const { data, error, isLoading } = useQuery( $api.queryOptions("get", "/users/{user_id}", { params: { path: { user_id: 5 } }, }), ); if (!data || isLoading) return "Loading..."; if (error) return `An error occured: ${error.message}`; return <div>{data.firstname}</div>; };

配合useQueries批量查询(例如按 ID 列表批量拉取用户):

import { useQueries } from '@tanstack/react-query'; import { $api } from "./api"; export const useUsersById = (userIds: number[]) => ( useQueries({ queries: userIds.map((userId) => ( $api.queryOptions("get", "/users/{user_id}", { params: { path: { user_id: userId } }, }) )) }) );

由于每个queryOptions的 queryKey 都包含不同的params,批量查询会生成相互独立的缓存条目——测试 test/index.test.tsx 验证了传入 4 个不同查询时queryClient.isFetching()为 4,且各自data/error类型正确。

配合fetchQuery手动取数:

const data = await queryClient.fetchQuery( $api.queryOptions("get", "/blogposts/{post_id}", { params: { path: { post_id: 5 } }, }) );

值得注意的细节(docs/openapi-react-query/query-options.md):useQuery与useSuspenseQuery内部都复用了queryOptions来构造 options。从源码看,queryOptions把init === undefined ? [method, path] : [method, path, init]作为queryKey(即无参数时 key 长度为 2,有参数时长度为 3,测试 test/index.test.tsx 验证了这一点),并共享同一个queryFn(src/index.ts)。这意味着同一个接口、同一组参数天然共享缓存——useQuery与fetchQuery、useQueries之间不存在 key 格式差异。

源码级原理:类型安全从何而来

阅读 src/index.ts 可以完整还原「类型安全」的实现机制:

1. QueryKey 类型

export type QueryKey< Paths extends Record<string, Record<HttpMethod, {}>>, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>, > = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];

(src/index.ts)

它把「方法 + 路径(+ 参数)」直接编码进 query key 的类型:Method被限制为HttpMethod,Path被限制为「该 Method 下存在的路径」(来自openapi-typescript-helpers的PathsWithMethod)。所以 key 本身就是 Schema 的类型投影,拼错即编译失败。

2. 统一 queryFn 与错误处理

所有查询共享同一个 fetcher 逻辑(src/index.ts):

const queryFn = async ({ queryKey: [method, path, init], signal }) => { const mth = method.toUpperCase(); const fn = client[mth]; const { data, error, response } = await fn(path, { signal, ...(init as any) }); if (error) throw error; // 失败抛错 → error 状态 if (response.status === 204 || response.headers.get("Content-Length") === "0") { return data ?? null; // 空响应 → data 为 null } return data; };

三个关键行为与测试一一对应:

  • 请求失败时抛出error:TanStack Query 会把抛出的错误放入error状态,于是data为undefined、error有值(测试 test/index.test.tsx);
  • 204 或Content-Length: 0的空响应返回null:data/error均为null(测试 test/index.test.tsx);
  • 非空响应但 body 为undefined:数据缺失被视为异常,data为undefined且error为Error实例(测试 test/index.test.tsx)。

3. 请求取消(AbortSignal)

queryFn会把 TanStack Query 的signal透传给 openapi-fetch,组件卸载或查询取消时自动中断请求。测试 test/index.test.tsx 验证了 unmount 后传给 fetch 的signal.aborted为true——这对大列表、快速切换路由的应用非常实用。

4. MethodResponse 类型工具

包还导出了MethodResponse<CreatedClient, Method, Path>类型(src/index.ts),用于从客户端实例反推某个接口的成功响应数据类型,方便在自定义 Hook、事件处理或非组件代码中引用:

import type { MethodResponse } from "openapi-react-query"; type Post = MethodResponse<typeof $api, "get", "/blogposts/{post_id}">; // → { title: string; body: string; publish_date?: number }

测试 test/index.test.tsx 即用它断言了useQuery的data类型等价于string[]。

验证与测试体系

该包用 Vitest + @testing-library/react + MSW(Mock Service Worker)构建了完整的类型与行为测试(vitest.config.ts、test/fixtures/mock-server.ts)。pnpm test会先执行generate-types用 openapi-typescript 重新生成夹具类型(package.json),保证测试始终基于最新 Schema 编译产物。测试覆盖了:

  • 类型层:错误的 method/path、缺失的必填参数、select返回值推导、queryKey长度等均通过@ts-expect-error与expectTypeOf断言(test/index.test.tsx);
  • 行为层:成功/失败/空响应的data、error状态机,mutation 的mutate/mutateAsync,无限分页的游标注入与自定义pageParamName,自定义queryClient透传等。

如果你要在自己的项目中复现这些场景,可以参照 test/fixtures/api.yaml 组织一份最小 Schema,再按上文「安装与初始化」流程生成类型并接入组件。

常见注意事项

  1. 版本要求:peerDependencies 要求@tanstack/react-query@^5.80.0与openapi-fetch(workspace 版本,即同仓库当前版本),请确保使用兼容的版本组合(package.json);
  2. 必填参数感知:options参数「有时必填有时可选」是类型层面的动态行为,取决于 Schema 中该接口是否有必填参数,请留意编辑器提示而非死记规则;
  3. 缓存一致性:useQuery、useSuspenseQuery与queryOptions共享同一套 query key 规则([method, path, params]),跨 API 组合使用不会出现 key 冲突;
  4. Suspense 需要 ErrorBoundary:useSuspenseQuery的错误通过抛异常传播,务必在组件树上层配置 ErrorBoundary,否则请求失败会直接导致渲染崩溃;
  5. 开启 noUncheckedIndexedAccess:官方强烈推荐,配合该库可获得最严格的空值检查体验(docs/advanced.md)。

总结

openapi-react-query 的价值在于把「Schema → 类型 → React 数据请求」这条链路完全打通:createClient从 fetch 客户端推导类型,五大 API 在继承 TanStack Query 全部能力的同时,把 URL、参数、请求体、响应体的类型检查下沉到编译期,并内置了空响应处理、请求取消、分页游标注入等贴心细节。对任何以 OpenAPI 规范维护接口的 React/React Native 项目,它都能显著减少样板代码与运行时错误,让 API 变更第一时间反映为编译错误。

进一步阅读:官方文档完整版见 docs/openapi-react-query/index.md,各 API 专项文档为 use-query.md、use-mutation.md、use-suspense-query.md、use-infinite-query.md 与 query-options.md;源码与测试分别在 src/index.ts 和 test/index.test.tsx。

  • 开发工具
  • 代码生成
  • 后端

【免费下载链接】openapi-typescript

Generate TypeScript types from OpenAPI 3 specs

项目地址:https://gitcode.com/gh_mirrors/op/openapi-typescript
点击查看免费下载
上一篇:PDF补丁丁:免费开源的PDF文档终极处理工具指南
下一篇:如何使用Latitude:面向开发者的嵌入式分析终极框架

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 2:05:22

8GB显卡跑27B三元量化模型:llama.cpp实测与性能边界分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:04:54

基于YOLO11的半导体晶圆缺陷检测:从数据集到PyQt5桌面端

简介&#xff1a;本资源是一套基于YOLO11深度学习构建的半导体晶圆外观缺陷检测系统&#xff0c;面向计算机、人工智能、自动化、电子信息等专业的在校学生、教师及企业技术人员&#xff0c;也适合作为毕业设计、课程设计或实战演示项目。系统可识别中心、甜甜圈、边缘位置、边…

作者头像 李华
网站建设 2026/9/26 2:04:54

大学四年职业规划指南:从大一到大四的关键动作与避坑策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:02:38

SDR++零基础实战指南:从接上第一台SDR到收听FM广播

SDR零基础实战指南&#xff1a;从接上第一台SDR到收听FM广播 【免费下载链接】SDRPlusPlus Cross-Platform SDR Software 项目地址: https://gitcode.com/GitHub_Trending/sd/SDRPlusPlus 把一根 RTL-SDR 插在电脑上&#xff0c;屏幕里却只有一片乱码——这是大多数人第…

作者头像 李华