next-safe-action
website·Indexed 33 days ago
https://next-safe-action.devA library that simplifies Server Actions in Next.js by providing end-to-end type safety, input and output validation, and a structured way to handle server-side logic. It includes a SafeActionClient for defining actions, middleware support for pre- and post-validation, and a Hooks API (useAction, useOptimisticAction, useStateAction) for executing actions from client components. Requirements include Next.js 14 (App Router), React 18.2.0, and TypeScript 5.
What's inside next-safe-action
- next-safe-action is a library designed to simplify Next.js Server Actions by providing end-to-end type safety from input validation to the action result. It utilizes Standard Schema (supporting Zod, Valibot, ArkType, etc.) for validation and offers a chainable API for middleware and metadata. Key features include full TypeScript inference, built-in input/output validation, type-safe context accumulation via middleware, and dedicated React hooks for action execution, status tracking, and optimistic updates.
Understand the next-safe-action execution lifecycle
When a safe action is invoked, it follows a strict server-side pipeline before returning a result to the client:
- Client Call: The action is triggered from a Client Component (via direct call,
useAction, or a form). - Middleware: The
.use()middleware stack executes in order. Layers can extend context or short-circuit the request. - Validate Input: Raw input is parsed and validated using Standard Schema.
- Validated Middleware: If validation succeeds, the
.useValidated()middleware stack runs with access to the typedparsedInputand accumulated context (ctx). - Server Code: The core async action function executes using the validated input and context.
- Validate Output: If an
outputSchemais provided, the return value is validated before being sent back to the client.
If any step fails, the pipeline stops and returns an error in the action result.
- Client Call: The action is triggered from a Client Component (via direct call,
Choose between built-in hooks and TanStack Query adapter
Decide which approach to use for handling server action mutations based on your project's architecture:
Prefer built-in hooks when:
- You want no extra dependencies (React only).
- You need deep integration with React's concurrent rendering model (e.g.,
startTransition). - Your app primarily uses Server Components for data fetching and only needs actions for simple mutations.
- You prefer direct access to the result envelope (
result.serverError,result.validationErrors) without error transformation.
Prefer the TanStack Query adapter when:
- TanStack Query is already in your stack (e.g., using
useQueryfor data fetching). - You want mutations to share the same client-side cache lifecycle (query invalidation, optimistic updates, retry strategies).
- You want to use TanStack Query DevTools for inspecting mutations.
- You need offline mutation persistence via
dehydrate/hydrate.
Initialize a SafeActionClient
TheSafeActionClientis the core ofnext-safe-action. It provides a chainable, immutable API for building type-safe server actions. Every method call returns a new client instance, ensuring the original client remains unmodified.import { createSafeActionClient } from "next-safe-action"; const actionClient = createSafeActionClient();Use formatValidationErrors for explicit default shapes
formatValidationErrors()is an identity function that returns validation errors as-is. It is used as a named reference when you want to be explicit about using the default validation error shape in your configuration.Handle Next.js navigation functions in safe actions
Next.js navigation functions such asredirect(),notFound(),forbidden(), andunauthorized()are supported within safe actions.next-safe-actionautomatically detects these internal Next.js errors, re-throws them so the framework can perform the navigation, and notifies the client via hooks or server-side callbacks."use server"; import { redirect } from "next/navigation"; import { actionClient } from "@/lib/safe-action"; export const loginAction = actionClient .inputSchema(loginSchema) .action(async ({ parsedInput }) => { const user = await authenticate(parsedInput); if (!user) { return returnValidationErrors(loginSchema, { _errors: ["Invalid credentials"], }); } // Triggers framework navigation redirect("/dashboard"); });Use Standard Schema validation libraries with next-safe-action
next-safe-action supports any validation library that implements the Standard Schema (v1) specification. This allows you to use libraries such as Zod, Valibot, or ArkType directly within.inputSchema()and.outputSchema()without requiring additional adapters or plugins. Because the integration happens at the protocol level, you can even mix different validation libraries for different actions within the same project.Use multiple validation libraries with next-safe-action v6
Starting with version 6, next-safe-action integrates the TypeSchema library, allowing the use of various validation libraries (such as Zod, Yup, and Valibot) within the same project. Existing Zod-based actions remain compatible as Zod is supported by TypeSchema.Narrow the result type using useAction status
TheuseActionreturn object is a discriminated union. Checking thestatusor shorthand booleans (likehasSucceededorhasErrored) narrows theresulttype, allowing safe access todata,serverError, orvalidationErrorswithout manual type casting.const action = useAction(myAction); // Narrowing via status if (action.status === "hasSucceeded") { action.result.data; // Guaranteed present action.result.serverError; // undefined action.result.validationErrors; // undefined } // Narrowing via shorthand booleans if (action.hasErrored) { if (action.result.serverError) { action.result.validationErrors; // undefined } }Implement stateful form actions with useStateAction
TheuseStateActionhook provides deep integration with the<form action={formAction}>prop. It offers full lifecycle callbacks (onSuccess,onError), status tracking (isPending,hasSucceeded,hasErrored), and intercepts navigation errors (like redirects) to enableonNavigationcallbacks. Note that because this is a client-side wrapper, it does not support no-JS progressive enhancement. To use this, define the server action using.stateAction()instead of.action().// src/app/actions.ts "use server"; import { z } from "zod"; import { zfd } from "zod-form-data"; import { actionClient } from "@/lib/safe-action"; const schema = zfd.formData({ email: zfd.text(z.string().email()), password: zfd.text(z.string().min(8)), }); export const loginAction = actionClient .inputSchema(schema) .stateAction(async ({ parsedInput: { email, password } }, { prevResult }) => { const user = await authenticate(email, password); return { userId: user.id, previousAttempt: prevResult.data?.userId }; });// src/app/login.tsx "use client"; import { useStateAction } from "next-safe-action/hooks"; import { loginAction } from "./actions"; export default function LoginForm() { const { formAction, result, isPending, hasSucceeded, hasErrored, reset } = useStateAction(loginAction, { onSuccess: ({ data }) => { console.log("Logged in as user", data.userId); }, onError: ({ error }) => { console.error("Login failed:", error.serverError); }, }); return ( <form action={formAction}> <input name="email" type="email" placeholder="Email" /> {result.validationErrors?.email && <p>{result.validationErrors.email._errors[0]}</p>} <input name="password" type="password" placeholder="Password" /> {result.validationErrors?.password && <p>{result.validationErrors.password._errors[0]}</p>} <button type="submit" disabled={isPending}> {isPending ? "Logging in..." : "Log in"} </button> {hasSucceeded && <p>Logged in as user {result.data.userId}</p>} {hasErrored && result.serverError && <p>Error: {result.serverError}</p>} {!isPending && (hasSucceeded || hasErrored) && ( <button type="button" onClick={reset}>Try again</button> )} </form> ); }Use useStateAction for stateful forms and serialized writes
The
useStateActionhook is used for actions defined with.stateAction(). It is built on React'suseActionStateand queues dispatches, ensuring that each write waits for the previous one to settle. This prevents race conditions and is ideal for forms, multi-step wizards, or any scenario where the server needs access to the previous result (prevResult).Note:
useStateActionrequires JavaScript and does not support no-JS progressive enhancement. For no-JS support, use React'suseActionStatedirectly."use client"; import { useStateAction } from "next-safe-action/hooks"; import { myStatefulAction } from "./actions"; export default function MyForm() { const { formAction, result, status, isPending, hasSucceeded, reset } = useStateAction(myStatefulAction, { onSuccess: ({ data }) => { console.log("Success:", data); }, onError: ({ error }) => { console.error("Error:", error.serverError); }, }); return ( <form action={formAction}> <input name="email" type="email" placeholder="Email" /> {result.validationErrors?.email && <p>{result.validationErrors.email._errors[0]}</p>} <button type="submit" disabled={isPending}> {isPending ? "Submitting..." : "Submit"} </button> {hasSucceeded && <p>Success!</p>} </form> ); }Install zod-form-data for file validation
Thezod-form-datalibrary is used to validateFormDatainputs, including file fields, within next-safe-action schemas.npm install zod-form-data # or pnpm add zod-form-data # or yarn add zod-form-data # or bun add zod-form-data