Loading...
Loading...
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
npx skill4agent add next-safe-action/skills safe-action-tanstack-querynpm install @next-safe-action/adapter-tanstack-query @tanstack/react-queryimport {
mutationOptions,
ActionMutationError,
isActionMutationError,
hasServerError,
hasValidationErrors,
} from "@next-safe-action/adapter-tanstack-query";"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>
);
}mutationOptions()UseMutationOptionsmutate()mutateAsync()serverErrorvalidationErrorsActionMutationErrorinstanceofdataTDataredirect()notFound()throwOnError| 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 |
| 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) | |
mutationOptions()queryOptions()POSTGETPOSTETaguseQuery| Entry point | Exports | Environment |
|---|---|---|
| | Client |
throwValidationErrors: truethrowServerError: truemutationOptions()NonThrowingActionConstraint// 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));