safe-action-tanstack-query logo

safe-action-tanstack-query

safe action tanstack query

next-safe-action/skills168installs0stars

SKILL.md

Full skill instructions

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

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