safe-action-tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

next-safe-action TanStack Query Adapter

next-safe-action TanStack Query 适配器

Install

安装

bash
npm install @next-safe-action/adapter-tanstack-query @tanstack/react-query
bash
npm install @next-safe-action/adapter-tanstack-query @tanstack/react-query

Import

导入

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()
creates a complete
UseMutationOptions
object that bridges next-safe-action's result envelope to TanStack Query's error model:
  1. Calls the safe action with input provided to
    mutate()
    /
    mutateAsync()
  2. Inspects the result envelope for
    serverError
    or
    validationErrors
  3. Throws
    ActionMutationError
    if either is present (a client-side error class,
    instanceof
    works)
  4. Returns
    data
    directly as TanStack Query's
    TData
    on success
  5. Handles navigation errors (
    redirect()
    ,
    notFound()
    , etc.) by composing
    throwOnError
    to always re-throw them during React's render phase
mutationOptions()
会创建一个完整的
UseMutationOptions
对象,将next-safe-action的结果封装与TanStack Query的错误模型衔接起来:
  1. 调用安全动作,入参为传递给
    mutate()
    /
    mutateAsync()
    的输入值
  2. 检查结果封装 中是否存在
    serverError
    validationErrors
  3. 如果存在上述任意一种错误则抛出
    ActionMutationError
    (这是一个客户端错误类,支持
    instanceof
    判断)
  4. 调用成功时直接将
    data
    作为TanStack Query的
    TData
    返回
  5. 处理导航错误
    redirect()
    notFound()
    等),通过组合
    throwOnError
    在React渲染阶段始终重新抛出这类错误

When to Use Which

适用场景选型

ScenarioRecommendation
New Next.js project without TanStack QueryBuilt-in hooks
Simple form submissions and button actionsBuilt-in hooks
Instant optimistic UI via React's
useOptimistic
Built-in hooks (
useOptimisticAction
)
Zero additional dependenciesBuilt-in hooks
Already using TanStack Query for data fetchingAdapter
Already using tRPC + TanStack QueryAdapter
Need automatic retries with backoffAdapter
Need to invalidate client query cache after mutationsAdapter
Want TanStack Query DevTools for mutationsAdapter
Need offline mutation persistenceAdapter
场景推荐方案
未使用TanStack Query的新Next.js项目内置钩子
简单表单提交和按钮动作内置钩子
通过React的
useOptimistic
实现即时乐观UI
内置钩子(
useOptimisticAction
零额外依赖内置钩子
已使用TanStack Query做数据获取适配器
已使用tRPC + TanStack Query适配器
需要带退避策略的自动重试适配器
需要在变更操作后失效客户端查询缓存适配器
想要使用TanStack Query DevTools查看变更操作适配器
需要离线变更持久化能力适配器

Feature Comparison

功能对比

FeatureBuilt-in hooksAdapter
React TransitionsYes, actions run inside
startTransition
No
Optimistic updates
useOptimisticAction
via React's
useOptimistic
Manual via
onMutate
+ query cache
Automatic retriesNoYes,
retry
option with backoff
Server cache invalidationYes,
revalidatePath()
/
revalidateTag()
Yes, same Next.js APIs
Client query cache invalidationNo (not applicable)Yes,
queryClient.invalidateQueries()
DevToolsNoYes, TanStack Query DevTools
Error modelResult envelope (
result.serverError
,
result.validationErrors
)
Thrown
ActionMutationError
with type guards
Offline mutation persistenceNoYes, paused mutations via
dehydrate
/
hydrate
Async execution
executeAsync()
returns
Promise<Result>
mutateAsync()
returns
Promise<Data>
Status tracking
status
string + shorthand booleans
Boolean flags (
isPending
,
isError
,
isSuccess
)
Extra dependenciesNone (React only)
@tanstack/react-query
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支持,动作在
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应用优先选择内置钩子,它们不需要额外依赖,且可与React的并发渲染集成。当你的技术栈中已经使用TanStack Query时优先选择适配器,尤其适用于需要缓存失效、重试、DevTools和离线支持的场景。

Why Mutations Only

为什么仅支持变更操作

This adapter provides only
mutationOptions()
. There is no
queryOptions()
, by design:
  • Server Actions use
    POST
    only, not suitable for
    GET
    -based queries
  • Server Actions are queued per client, creating request waterfalls
  • POST
    requests bypass browser cache,
    ETag
    , and conditional requests
  • No stable resource identity for TanStack Query deduplication
For data fetching: use Server Components (server-side), Route Handlers +
useQuery
(client-side), or tRPC (full-stack type-safe).
本适配器仅提供
mutationOptions()
设计上并未提供
queryOptions()
,原因如下:
  • Server Actions仅支持
    POST
    方法,不适合基于
    GET
    的查询场景
  • Server Actions按客户端排队执行,会产生请求瀑布流问题
  • POST
    请求会绕过浏览器缓存、
    ETag
    和条件请求机制
  • 不存在可供TanStack Query去重的稳定资源标识
数据获取方案推荐:使用服务端组件(服务端侧)、路由处理器 +
useQuery
(客户端侧),或者tRPC(全栈类型安全)。

Entry Points

入口文件

Entry pointExportsEnvironment
@next-safe-action/adapter-tanstack-query
mutationOptions
,
ActionMutationError
,
isActionMutationError
,
hasServerError
,
hasValidationErrors
, types
Client
入口导出内容运行环境
@next-safe-action/adapter-tanstack-query
mutationOptions
ActionMutationError
isActionMutationError
hasServerError
hasValidationErrors
、相关类型
客户端

Important Constraints

重要约束

Only works with non-throwing actions. Do NOT use
throwValidationErrors: true
or
throwServerError: true
with actions passed to
mutationOptions()
. 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
NonThrowingActionConstraint
.
仅适用于非抛错型动作。不要将设置了
throwValidationErrors: true
throwServerError: true
的动作传递给
mutationOptions()
。适配器会从结果封装中检查错误,如果错误是被抛出而不是返回的,适配器无法提取结构化错误数据,你也会失去类型安全的错误处理能力。TypeScript会通过
NonThrowingActionConstraint
规则强制校验这一点。

Supporting 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));