关于 safe-action-tanstack-query
safe-action-tanstack-query 是将 next-safe-action 的 Server Actions 接入 TanStack Query mutation 的适配器,适用于已使用 TanStack Query 的 Next.js 项目。它将结果信封中的服务端或校验错误转换为带类型的错误。
主要功能
- 01生成 TanStack Query mutation 选项
- 02将服务端与校验错误转为 ActionMutationError
- 03提供错误类型判断工具
- 04支持重试、缓存失效与离线 mutation
适用场景
- 01在 TanStack Query 项目中调用 Server Actions
- 02根据 mutation 结果失效客户端查询缓存
- 03为失败的 mutation 配置重试
- 04持久化离线期间暂停的 mutation
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ next-safe-action/ skills --skill safe-action-tanstack-query安装后在 Claude Code 中输入/safe-action-tanstack-query确认已加载。
附带文件mutation-patterns.md
详细介绍
next-safe-action TanStack Query 适配器
安装
npm install @next-safe-action/adapter-tanstack-query @tanstack/react-query
导入
import {
mutationOptions,
ActionMutationError,
isActionMutationError,
hasServerError,
hasValidationErrors,
} from "@next-safe-action/adapter-tanstack-query";
快速开始
"use client";
import { useMutation } from "@tanstack/react-query";
import { mutationOptions } from "@next-safe-action/adapter-tanstack-query";
import { createUser } from "@/app/actions";
export function CreateUserForm() {
const { mutate, isPending, isError, error, data } = useMutation(
mutationOptions(createUser, {
onSuccess: (data) => {
toast.success(`Created ${data.name}`);
},
})
);
return (
<form onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
mutate({ name: fd.get("name") as string, email: fd.get("email") as string });
}}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{isError && <p className="text-red-500">{error.message}</p>}
{data && <p>Created: {data.name}</p>}
</form>
);
}
工作原理
mutationOptions() 会创建一个完整的 UseMutationOptions 对象,将 next-safe-action 的结果封装转换为 TanStack Query 的错误模型:
- 调用安全操作,并传入
mutate()/mutateAsync()提供的输入 - 检查结果封装中是否存在
serverError或validationErrors - 如果其中任一项存在,则抛出
ActionMutationError(一种客户端错误类,可使用instanceof判断) - 成功时直接返回
data,作为 TanStack Query 的TData - 处理导航错误(
redirect()、notFound()等):组合throwOnError,确保它们始终在 React 的渲染阶段重新抛出
通过 returnServerError() 生成的预期错误与其他服务器错误一样会进入 serverError,因此会以带有类型化载荷的 ActionMutationError 形式呈现,与 mutationOptions() 完全兼容(与 throwServerError 不同)。
该如何选择
| 场景 | 建议 |
|---|---|
| 不使用 TanStack Query 的新 Next.js 项目 | 内置 Hooks |
| 简单的表单提交和按钮操作 | 内置 Hooks |
通过 React 的 useOptimistic 实现即时乐观 UI | 内置 Hooks(useOptimisticAction) |
| 必须排队并累积的重叠写入(重新排序、看板) | 内置 Hooks(useOptimisticStateAction) |
| 独立于用户而变化的数据(套接字、轮询、后台刷新) | 使用 TanStack Query / SWR 获取数据,使用适配器处理变更 |
| 不引入额外依赖 | 内置 Hooks |
| 已使用 TanStack Query 获取数据 | 适配器 |
| 已使用 tRPC + TanStack Query | 适配器 |
| 需要自动重试和退避 | 适配器 |
| 需要在变更后使客户端查询缓存失效 | 适配器 |
| 希望使用 TanStack Query DevTools 查看变更 | 适配器 |
| 需要离线变更持久化 | 适配器 |
功能对比
| 功能 | 内置 Hooks | 适配器 |
|---|---|---|
| React Transitions | 是,操作在 startTransition 中运行 | 否 |
| 乐观更新 | 通过 React 的 useOptimistic 使用 useOptimisticAction | 通过 onMutate + 查询缓存手动实现 |
| 自动重试 | 否 | 是,使用带退避的 retry 选项 |
| 服务端缓存失效 | 是,使用 revalidatePath() / revalidateTag() | 是,使用相同的 Next.js API |
| 客户端查询缓存失效 | 否(不适用) | 是,使用 queryClient.invalidateQueries() |
| DevTools | 否 | 是,TanStack Query DevTools |
| 错误模型 | 结果封装(result.serverError、result.validationErrors) | 抛出带类型守卫的 ActionMutationError |
| 离线变更持久化 | 否 | 是,通过 dehydrate/hydrate 暂停变更 |
| 异步执行 | executeAsync() 返回 Promise<Result> | mutateAsync() 返回 Promise<Data> |
| 状态跟踪 | status 字符串 + 简写布尔值 | 布尔标志(isPending、isError、isSuccess) |
| 额外依赖 | 无(仅 React) | @tanstack/react-query |
**一般建议:**大多数 Next.js 应用优先使用内置 Hooks。它们无需额外依赖,并且能与 React 的并发渲染集成。如果技术栈中已包含 TanStack Query,尤其是需要缓存失效、重试、DevTools 和离线支持时,优先使用适配器。
为什么只支持变更
此适配器仅提供 mutationOptions(),这是有意为之,因此没有 queryOptions():
- Server Actions 仅使用
POST,不适用于基于GET的查询 - Server Actions 会按客户端排队,造成请求瀑布流
POST请求会绕过浏览器缓存、ETag和条件请求- TanStack Query 去重所需的稳定资源标识不存在
获取数据时:使用 Server Components(服务端)、Route Handlers + useQuery(客户端),或 tRPC(全栈类型安全)。
入口
| 入口 | 导出 | 环境 |
|---|---|---|
@next-safe-action/adapter-tanstack-query | mutationOptions、ActionMutationError、isActionMutationError、hasServerError、hasValidationErrors、类型 | 客户端 |
重要限制
仅适用于无状态的 .action() 函数。 mutationOptions() 接受 SingleInputActionFn,因此无法包装 .stateAction()。对于有状态的操作,请使用 next-safe-action/hooks 中的 useStateAction / useOptimisticStateAction。
仅适用于不会抛出异常的操作。 请勿对传递给 mutationOptions() 的操作使用 throwValidationErrors: true 或 throwServerError: true。适配器会检查结果封装中的错误。如果错误是抛出而不是返回的,适配器就无法提取结构化错误数据,你也会失去类型安全的错误处理能力。TypeScript 通过 NonThrowingActionConstraint 强制执行此限制。
相关文档
反模式
// BAD: Using throwValidationErrors with adapter — errors bypass the result envelope
const client = createSafeActionClient({ throwValidationErrors: true });
const action = client.inputSchema(schema).action(async ({ parsedInput }) => { ... });
mutationOptions(action); // TypeScript error! NonThrowingActionConstraint not met
// BAD: Using mutationOptions for data fetching — server actions are POST-only
const { data } = useQuery(mutationOptions(fetchUsers)); // Wrong! Use Route Handler + useQuery
// BAD: Manually calling the action inside mutationFn
useMutation({
mutationFn: async (input) => {
const result = await myAction(input); // Loses error bridging, navigation handling
return result.data;
},
});
// GOOD: Let mutationOptions handle the bridging
useMutation(mutationOptions(myAction));