Agent Skills

safe-action-tanstack-query

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

Install

npx skills add https://github.com/next-safe-action/skills --skill safe-action-tanstack-query
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

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 useOptimistic Built-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 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

Feature Comparison

Feature Built-in hooks Adapter
React Transitions Yes, actions run inside startTransition No
Optimistic updates useOptimisticAction via React's useOptimistic Manual via onMutate + query cache
Automatic retries No Yes, retry option with backoff
Server cache invalidation Yes, revalidatePath() / revalidateTag() Yes, same Next.js APIs
Client query cache invalidation No (not applicable) Yes, queryClient.invalidateQueries()
DevTools No Yes, TanStack Query DevTools
Error model Result envelope (result.serverError, result.validationErrors) Thrown ActionMutationError with type guards
Offline mutation persistence No Yes, 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 dependencies None (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 point Exports Environment
@next-safe-action/adapter-tanstack-query mutationOptions, ActionMutationError, isActionMutationError, hasServerError, hasValidationErrors, types Client

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

Related skills

vercel-react-best-practicesvercel-labs751KReact and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.vercel-composition-patternsvercel-labs361KReact composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.vercel-react-view-transitionsvercel-labs135KGuide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions viewpick-ui-libraryemilkowalski124KPick the right library for a given frontend task from a curated, opinionated list — numbers, OTP inputs, charts, command menus, virtualization, drag and drop, toasts, state, styling, and more. Only runs when explicitly invoked; it does not trigger on its own.

Search skills and MCP servers

Fuzzy search across 23,137 skills and servers