Supabase Studio React Query 数据获取规范:Query Keys、queryOptions 与 Mutation Hook 的源码级实践
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
本篇指南基于 Supabase 仓库中apps/studio/的数据层约定(源自.claude/skills/studio-queries/SKILL.md),系统讲解 Supabase Studio 控制台前端如何组织 React Query(TanStack Query v5)数据获取:按领域划分的keys.ts查询键工厂、queryOptions优先的查询定义模式、命令式fetchQuery取数、以及带自动缓存失效与错误兜底的 Mutation Hook 模板。读完你将掌握在apps/studio/data/下为任意新 API 端点编写首个 fetch 或 mutation 的完整套路,并能对照真实源码理解每一处约定的工程动机。
一、约定的适用范围与参照文件
该规范适用于apps/studio/data/下所有查询 Hook、Mutation Hook 与 query key 的新增与评审场景,包括「为一个新 API 端点或资源添加第一个 fetch 或 mutation」。规范指定了三个参照实现:
| 职责 | 参照文件 |
|---|---|
| Query options 模式 | table-editor-query.ts |
| Mutation hook 模板 | edge-functions-update-mutation.ts |
| Query keys 工厂 | keys.ts |
从源码结构看,apps/studio/data/按业务领域(edge-functions、projects、database、auth 等)拆分为大量同名目录,每个目录内聚keys.ts与各自的*-query.ts/*-mutation.ts文件,这是该规范能够落地的目录基础。
二、Query Keys:按领域定义 keys.ts
规范的第一条硬性要求:每个领域(domain)定义一个keys.ts,导出*Keys辅助函数;键用数组 +as const声明;组件中永远不要内联 query key。
规范给出的示例:
export const edgeFunctionsKeys = { list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const, detail: (projectRef: string | undefined, slug: string | undefined) => ['projects', projectRef, 'edge-function', slug, 'detail'] as const, }真实的 keys.ts 与示例完全一致,并在此基础上多出了body与lastHourStats两个键:
export const edgeFunctionsKeys = { list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const, lastHourStats: (projectRef: string | undefined, functionIds: string[] = [], useOtel: boolean = false) => ['projects', projectRef, 'edge-functions', 'last-hour-stats', normalizeFunctionIds(functionIds), { otel: useOtel }] as const, detail: (projectRef: string | undefined, slug: string | undefined) => ['projects', projectRef, 'edge-function', slug, 'detail'] as const, body: (projectRef: string | undefined, slug: string | undefined) => ['projects', projectRef, 'edge-function', slug, 'body'] as const, }这组代码展示了三个关键设计点:
- 层级前缀:所有键都以
['projects', projectRef, ...]开头。React Query 的invalidateQueries基于前缀匹配,因此 mutation 成功后只需失效xKeys.list(projectRef),即可自动覆盖该列表下所有 detail/body 派生缓存——这是「mutation 里同时失效 list 与 detail 两条键」约定的底层原因。 as const固化元组:让 query key 的类型成为字面量元组而非宽泛的QueryKey,从而在useQuery/fetchQuery调用处获得参数收窄与拼写检查。- 参数归一化:文件顶部的
normalizeFunctionIds(去重 + 排序)保证「同一组函数 id 不同顺序」产生同一个缓存键,避免缓存碎片化。这是对「query key 即缓存身份」这一语义的直接应用。
三、Query Options:推荐的查询定义模式
规范明确:优先使用@tanstack/react-query的queryOptions,因为它既能配合声明式的useQuery(),又能配合命令式的queryClient.fetchQuery(),且全程保持类型安全。
3.1 完整模板
规范给出的标准模板(原样继承自 SKILL.md):
import { queryOptions } from '@tanstack/react-query' import { xKeys } from './keys' import { get, handleError } from '@/data/fetchers' import { IS_PLATFORM } from '@/lib/constants' import { ResponseError } from '@/types' export type XVariables = { projectRef?: string } export type XError = ResponseError async function getX({ projectRef }: XVariables, signal?: AbortSignal) { if (!projectRef) throw new Error('projectRef is required') const { data, error } = await get('/v1/projects/{ref}/x', { params: { path: { ref: projectRef } }, signal, }) if (error) handleError(error) return data } export type XData = Awaited<ReturnType<typeof getX>> export const xQueryOptions = ({ projectRef }: XVariables) => queryOptions({ queryKey: xKeys.list(projectRef), queryFn: ({ signal }) => getX({ projectRef }, signal), enabled: IS_PLATFORM && typeof projectRef !== 'undefined', })模板配套五条规则:
- 导出
XVariables、XData、XError三个类型(以领域名作前缀)。XData通过Awaited<ReturnType<typeof getX>>推导——由于 HTTP 客户端是全类型生成的,返回值类型自动跟随 OpenAPI 类型变化,无需手写。 - 实现一个私有的
getX(variables, signal?)函数,要求:缺少必填变量时抛错;把signal透传给请求以支持取消;失败时调用handleError(error)(该函数会抛出,见下文 5.2);成功时返回data。 getX不导出——命令式取数应走queryClient.fetchQuery(xQueryOptions(...)),让请求与缓存策略保持一致。- 用
enabled做门控,保证必填变量未就绪前查询不会发出。 - 平台专属查询:把
IS_PLATFORM(来自 lib/constants,其定义为process.env.NEXT_PUBLIC_IS_PLATFORM === 'true')并入enabled,本地自托管部署下直接不发起平台 API 请求。 - 不要给
xQueryOptions追加额外参数——调用方需要覆盖时通过解构展开实现:{ ...xQueryOptions(vars), enabled: true }。这一条约束保证了 options 工厂的签名稳定、可复用。
3.2 真实实现佐证
table-editor-query.ts 是该模式的落地版本:它导出tableEditorQueryOptions(queryOptions工厂)、useTableEditorQuery(包装useQuery,在enabled上叠加typeof projectRef !== 'undefined' && typeof id !== 'undefined' && !isNaN(id)校验)、以及prefetchTableEditor(client, vars)(直接client.fetchQuery(tableEditorQueryOptions(...)),即私有getTableEditor被封装在 options 内、对外只暴露 options 的具体体现)。该 hook 还展示了按查询特点覆盖缓存策略的写法:
return useQuery<TableEditorData, TableEditorError, TData>({ ...tableEditorQueryOptions({ projectRef, connectionString, id, scoped }), enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined' && !isNaN(id), refetchOnWindowFocus: false, refetchOnMount: false, staleTime: 5 * 60 * 1000, ...options, })注意...options放在最后:这正是「调用方通过解构覆盖默认项」约定的体现——UseCustomQueryOptions类型把queryKey从 options 中剔除(见 types/react-query.ts),调用方因此无法在覆盖时意外改动查询键,只能覆盖enabled、staleTime等行为项。
四、在组件中使用 Query Options
规范推荐的组件侧用法:
import { useQuery } from '@tanstack/react-query' import { xQueryOptions } from '@/data/x/x-query' const { data, isPending, isError } = useQuery(xQueryOptions({ projectRef: project?.ref }))组件层使用规则同样被明确:
- 使用 React Query v5 的 flag 命名:
isPending表示首次加载(尚无数据),isFetching表示后台刷新(已有数据、正在重取); - 按固定顺序显式渲染三态:pending → error → success,避免三态渲染互相覆盖导致的状态闪烁。
五、命令式取数(组件外或回调中)
在回调、非 React 上下文或「先取数再决定后续动作」的场景,规范要求走queryClient.fetchQuery而非直接调 API,示例:
const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const handleClick = useCallback( async (id: number) => { const data = await queryClient.fetchQuery(xQueryOptions({ id, projectRef: project?.ref })) // use data... }, [project?.ref, queryClient] )这样做的收益是:取数结果进入缓存(后续useQuery可命中)、继承全局重试/staleTime策略、且与声明式查询共享同一个 query key。真实代码中 table-editor-query.ts 的prefetchTableEditor就是同款用法的路由级预取版本。
六、Mutation Hook 模板
规范对 mutation 的要求:
- 导出一个
Variables类型,包含projectRef、资源标识符(如slug)与payload; - 实现私有的
updateX(vars),内含必填变量校验与handleError; - 包装为
useXMutation():接收去掉mutationFn的UseMutationOptions;在onSuccess中用await Promise.all([...])同时失效list()与detail()两条键;未提供onError时默认toast.error(...)。
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' import { xKeys } from './keys' type XUpdateVariables = { projectRef: string; slug: string; payload: XPayload } export const useXUpdateMutation = ({ onSuccess, onError, ...options }: UseMutationOptions<XData, XError, XUpdateVariables> = {}) => { const queryClient = useQueryClient() return useMutation({ mutationFn: updateX, async onSuccess(data, variables, context) { await Promise.all([ queryClient.invalidateQueries({ queryKey: xKeys.detail(variables.projectRef, variables.slug), }), queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }), ]) await onSuccess?.(data, variables, context) }, async onError(error, variables, context) { if (onError === undefined) toast.error(`Failed to update: ${error.message}`) else onError(error, variables, context) }, ...options, }) }一个实现细节值得注意:模板把...options放在配置对象末尾,意味着调用方传入的任何字段(如onMutate、retry)都可以覆盖默认行为,而内置的失效逻辑又先于onSuccess?.(...)执行——保证缓存刷新不会因调用方忘记处理而漏掉。
6.1 真实 Mutation 实现
edge-functions-update-mutation.ts 是该模板的参照实现,完整覆盖了规范要点:
export async function updateEdgeFunction({ projectRef, slug, payload }: EdgeFunctionsUpdateVariables) { if (!projectRef) throw new Error('projectRef is required') const { data, error } = await patch(`/v1/projects/{ref}/functions/{function_slug}`, { params: { path: { ref: projectRef, function_slug: slug } }, body: payload, }) if (error) handleError(error) return data }async onSuccess(data, variables, context) { const { projectRef, slug } = variables await Promise.all([ queryClient.invalidateQueries({ queryKey: edgeFunctionsKeys.detail(projectRef, slug) }), queryClient.invalidateQueries({ queryKey: edgeFunctionsKeys.list(projectRef) }), ]) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { if (onError === undefined) { toast.error(`Failed to update edge function: ${data.message}`) } else { onError(data, variables, context) } }对照模板有两处仓库现状差异,阅读代码时需注意:其一,当前实现从sonner引入toast(模板中写作react-hot-toast),说明 toast 库已演进,落地时以仓库实际依赖为准;其二,该文件的 options 类型是Omit<UseCustomMutationOptions<Data, ResponseError, Variables>, 'mutationFn'>,而 types/react-query.ts 中UseCustomMutationOptions已标注@deprecated,注释建议直接使用UseMutationOptions——模板的新写法(直接UseMutationOptions)即代表收敛方向。payload类型{ name?, verify_jwt?, import_map? }也与 OpenAPI 的FunctionUpdate端点对应。
七、底层支撑:fetchers 与全局 QueryClient
模板里的get/handleError并非空泛约定,它们对应 fetchers.ts 中的真实实现,理解这两点能解释为什么模板可以放心地「失败时只写一行if (error) handleError(error)」:
- 类型化 HTTP 客户端:
fetchers.ts用openapi-fetch基于生成的paths类型创建客户端,导出GET/POST/PUT/PATCH/DELETE等具名方法(文件底部解构为get, post, put, patch, del),路径模板/v1/projects/{ref}/functions/{function_slug}中的{ref}占位符由params.path填充,参数与返回类型全链路推导。中间件统一注入Authorization、X-Request-Id,并在响应侧补充code、requestId、retryAfter(取自Retry-After/X-RateLimit-Reset头)、requestPathname等字段。 handleError是「抛错函数」:其签名为(error, options?) => never。它按ERROR_PATTERNS(错误消息模式表)把错误映射到具体错误类,无法匹配时落到UnknownAPIResponseError;对于没有可展示消息的未知错误会记录 Sentry 并抛出消息被刻意模糊化的通用错误,防止不可信的服务端内容直接进入 UI。正因它永远抛出,模板中「成功返回data、失败交给handleError」的二分法才成立。
全局缓存行为定义在 query-client.ts 的getQueryClient()中,这也是模板不需要每个查询重复配置的基础默认值:
staleTime: 60 * 1000(1 分钟)作为全局默认;- 重试策略:4xx(除 429 外)不重试——429 需按
retryAfter退避重试,注释解释了原因:若对限流立即失败,前端会重新发起新请求,反而放大限流风暴;特定重路径(如/platform/pg-meta/:ref/query)跳过重试以减负,但 429 例外;最多重试 3 次(MAX_RETRY_FAILURE_COUNT = 3); retryDelay:错误携带retryAfter时按其换算毫秒,否则指数退避Math.min(1000 * 2 ** failureCount, 30000);- 本地开发(
!IS_PLATFORM)时onlineManager.setOnline(true),模拟始终在线,避免离线态干扰调试。
八、落地清单
为一个新端点在apps/studio/data/<domain>/下新增取数时,按以下顺序核对即可覆盖规范全部要点:
- 在
keys.ts中用as const数组键补充list/detail等工厂函数,保持['projects', projectRef, ...]层级前缀; - 查询文件导出
XVariables/XData/XError,私有getX完成「校验必填变量 → 透传signal→ 失败走handleError」; xQueryOptions用queryOptions包装,enabled门控必填变量,平台专属查询叠加IS_PLATFORM;不扩展工厂参数,调用方解构覆盖;- 组件内
useQuery(xQueryOptions(...)),按 pending → error → success 渲染,区分isPending与isFetching; - 回调或非 React 场景用
queryClient.fetchQuery(xQueryOptions(...)); - Mutation 导出
Variables类型与私有updateX,onSuccess中Promise.all失效 list + detail 键,onError缺省toast.error; - 重试与退避交给全局 QueryClient 默认值,仅在查询语义特殊时(如 table-editor 的
staleTime: 5 分钟、关闭窗口聚焦重取)按查询覆盖。
整套约定本质上是把「缓存身份、取数逻辑、缓存策略、失效联动」四件事分别锁死在keys.ts、私有 fetch 函数、queryOptions工厂与 mutation 的onSuccess里,使apps/studio/data/下数百个领域文件保持同构、可机械评审,也为 LLM 或新成员按模板补写新端点代码提供了确定性基础。
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考