safe-action-tanstack-query

v2026.09.24

Use when integrating next-safe-action with TanStack Query (React Query) -- mutationOptions(), useMutation, ActionMutationError error handling, type guards, optimistic updates via query cache, query invalidation after mutations

GitHub
安装命令
npx skhub add next-safe-action/safe-action-tanstack-query
Markdown
SKILL.md

next-safe-action TanStack Query Adapter

Install

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

Import

import {
  mutationOptions,
  ActionMutationError,
  isActionMutationError,
  hasServerError,
  hasValidationErrors,
} from "@next-safe-action/adapter-tanstack-query";

Quick Start

"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

Expected errors produced with returnServerError() land in serverError like any other server error, so they surface as ActionMutationError with the typed payload — fully compatible with mutationOptions() (unlike throwServerError).

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 useOptimisticBuilt-in hooks (useOptimisticAction)
Overlapping writes that must be queued and accumulate (reorder, Kanban)Built-in hooks (useOptimisticStateAction)
Data that changes independently of the user (sockets, polling, background refresh)TanStack Query / SWR for the reads, adapter for the mutations
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

Feature Comparison

FeatureBuilt-in hooksAdapter
React TransitionsYes, actions run inside startTransitionNo
Optimistic updatesuseOptimisticAction via React's useOptimisticManual 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 executionexecuteAsync() returns Promise<Result>mutateAsync() returns Promise<Data>
Status trackingstatus string + shorthand booleansBoolean 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.

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).

Entry Points

Entry pointExportsEnvironment
@next-safe-action/adapter-tanstack-querymutationOptions, ActionMutationError, isActionMutationError, hasServerError, hasValidationErrors, typesClient

Important Constraints

Only works with stateless .action() functions. mutationOptions() accepts a SingleInputActionFn, so a .stateAction() cannot be wrapped. For stateful actions use useStateAction / useOptimisticStateAction from next-safe-action/hooks.

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.

Supporting Docs

Anti-Patterns

// 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));
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

未指定

源路径

skills/safe-action-tanstack-query

默认分支

main

最新提交

a2605bd

Tree SHA

2e27376