safe-action-tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesenext-safe-action TanStack Query Adapter
next-safe-action TanStack Query 适配器
Install
安装
bash
npm install @next-safe-action/adapter-tanstack-query @tanstack/react-querybash
npm install @next-safe-action/adapter-tanstack-query @tanstack/react-queryImport
导入
ts
import {
mutationOptions,
ActionMutationError,
isActionMutationError,
hasServerError,
hasValidationErrors,
} from "@next-safe-action/adapter-tanstack-query";ts
import {
mutationOptions,
ActionMutationError,
isActionMutationError,
hasServerError,
hasValidationErrors,
} from "@next-safe-action/adapter-tanstack-query";Quick Start
快速开始
tsx
"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>
);
}tsx
"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>
);
}How It Works
工作原理
mutationOptions()UseMutationOptions- Calls the safe action with input provided to /
mutate()mutateAsync() - Inspects the result envelope for or
serverErrorvalidationErrors - Throws if either is present (a client-side error class,
ActionMutationErrorworks)instanceof - Returns directly as TanStack Query's
dataon successTData - Handles navigation errors (,
redirect(), etc.) by composingnotFound()to always re-throw them during React's render phasethrowOnError
mutationOptions()UseMutationOptions- 调用安全动作,入参为传递给 /
mutate()的输入值mutateAsync() - 检查结果封装 中是否存在 或
serverErrorvalidationErrors - 如果存在上述任意一种错误则抛出(这是一个客户端错误类,支持
ActionMutationError判断)instanceof - 调用成功时直接将作为TanStack Query的
data返回TData - 处理导航错误(、
redirect()等),通过组合notFound()在React渲染阶段始终重新抛出这类错误throwOnError
When to Use Which
适用场景选型
| Scenario | Recommendation |
|---|---|
| New Next.js project without TanStack Query | Built-in hooks |
| Simple form submissions and button actions | Built-in hooks |
Instant optimistic UI via React's | Built-in hooks ( |
| Zero additional dependencies | Built-in hooks |
| Already using TanStack Query for data fetching | Adapter |
| Already using tRPC + TanStack Query | Adapter |
| Need automatic retries with backoff | Adapter |
| Need to invalidate client query cache after mutations | Adapter |
| Want TanStack Query DevTools for mutations | Adapter |
| Need offline mutation persistence | Adapter |
| 场景 | 推荐方案 |
|---|---|
| 未使用TanStack Query的新Next.js项目 | 内置钩子 |
| 简单表单提交和按钮动作 | 内置钩子 |
通过React的 | 内置钩子( |
| 零额外依赖 | 内置钩子 |
| 已使用TanStack Query做数据获取 | 适配器 |
| 已使用tRPC + TanStack Query | 适配器 |
| 需要带退避策略的自动重试 | 适配器 |
| 需要在变更操作后失效客户端查询缓存 | 适配器 |
| 想要使用TanStack Query DevTools查看变更操作 | 适配器 |
| 需要离线变更持久化能力 | 适配器 |
Feature Comparison
功能对比
| Feature | Built-in hooks | Adapter |
|---|---|---|
| React Transitions | Yes, actions run inside | No |
| Optimistic updates | | Manual via |
| Automatic retries | No | Yes, |
| Server cache invalidation | Yes, | Yes, same Next.js APIs |
| Client query cache invalidation | No (not applicable) | Yes, |
| DevTools | No | Yes, TanStack Query DevTools |
| Error model | Result envelope ( | Thrown |
| Offline mutation persistence | No | Yes, paused mutations via |
| Async execution | | |
| Status tracking | | Boolean flags ( |
| Extra dependencies | None (React only) | |
General guidance: Prefer built-in hooks for most Next.js apps. They require no extra dependencies and integrate with React's concurrent rendering. Prefer the adapter when TanStack Query is already part of your stack, especially for cache invalidation, retries, DevTools, and offline support.
| 功能 | 内置钩子 | 适配器 |
|---|---|---|
| React Transitions | 支持,动作在 | 不支持 |
| 乐观更新 | 通过React的 | 通过 |
| 自动重试 | 不支持 | 支持,带退避策略的 |
| 服务端缓存失效 | 支持, | 支持,相同的Next.js API |
| 客户端查询缓存失效 | 不支持(不适用) | 支持, |
| DevTools | 不支持 | 支持,TanStack Query DevTools |
| 错误模型 | 结果封装( | 抛出带类型守卫的 |
| 离线变更持久化 | 不支持 | 支持,通过 |
| 异步执行 | | |
| 状态追踪 | | 布尔标识( |
| 额外依赖 | 无(仅依赖React) | |
通用指南:大多数Next.js应用优先选择内置钩子,它们不需要额外依赖,且可与React的并发渲染集成。当你的技术栈中已经使用TanStack Query时优先选择适配器,尤其适用于需要缓存失效、重试、DevTools和离线支持的场景。
Why Mutations Only
为什么仅支持变更操作
This adapter provides only . There is no , by design:
mutationOptions()queryOptions()- Server Actions use only, not suitable for
POST-based queriesGET - Server Actions are queued per client, creating request waterfalls
- requests bypass browser cache,
POST, and conditional requestsETag - No stable resource identity for TanStack Query deduplication
For data fetching: use Server Components (server-side), Route Handlers + (client-side), or tRPC (full-stack type-safe).
useQuery本适配器仅提供。设计上并未提供,原因如下:
mutationOptions()queryOptions()- Server Actions仅支持方法,不适合基于
POST的查询场景GET - Server Actions按客户端排队执行,会产生请求瀑布流问题
- 请求会绕过浏览器缓存、
POST和条件请求机制ETag - 不存在可供TanStack Query去重的稳定资源标识
数据获取方案推荐:使用服务端组件(服务端侧)、路由处理器 + (客户端侧),或者tRPC(全栈类型安全)。
useQueryEntry Points
入口文件
| Entry point | Exports | Environment |
|---|---|---|
| | Client |
| 入口 | 导出内容 | 运行环境 |
|---|---|---|
| | 客户端 |
Important Constraints
重要约束
Only works with non-throwing actions. Do NOT use or with actions passed to . The adapter inspects the result envelope for errors. If errors are thrown instead of returned, the adapter cannot extract structured error data, and you lose type-safe error handling. TypeScript enforces this via .
throwValidationErrors: truethrowServerError: truemutationOptions()NonThrowingActionConstraint仅适用于非抛错型动作。不要将设置了或的动作传递给。适配器会从结果封装中检查错误,如果错误是被抛出而不是返回的,适配器无法提取结构化错误数据,你也会失去类型安全的错误处理能力。TypeScript会通过规则强制校验这一点。
throwValidationErrors: truethrowServerError: truemutationOptions()NonThrowingActionConstraintSupporting Docs
相关文档
- Mutation patterns, error handling, and optimistic updates
- 变更模式、错误处理与乐观更新
Anti-Patterns
反模式
ts
// 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));ts
// 错误:在适配器中使用throwValidationErrors —— 错误会绕过结果封装
const client = createSafeActionClient({ throwValidationErrors: true });
const action = client.inputSchema(schema).action(async ({ parsedInput }) => { ... });
mutationOptions(action); // TypeScript报错!不满足NonThrowingActionConstraint要求
// 错误:使用mutationOptions做数据获取 —— server actions仅支持POST
const { data } = useQuery(mutationOptions(fetchUsers)); // 错误!请使用路由处理器 + useQuery
// 错误:在mutationFn内部手动调用动作
useMutation({
mutationFn: async (input) => {
const result = await myAction(input); // 丢失错误衔接、导航处理能力
return result.data;
},
});
// 正确:让mutationOptions处理衔接逻辑
useMutation(mutationOptions(myAction));