next-safe-action

website·Indexed 33 days ago

https://next-safe-action.dev

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

Tokens
43.1K
Snippets
190
Records
255
Agent score
67%

What's inside next-safe-action

  1. Overview of 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.
  2. 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:

    1. Client Call: The action is triggered from a Client Component (via direct call, useAction, or a form).
    2. Middleware: The .use() middleware stack executes in order. Layers can extend context or short-circuit the request.
    3. Validate Input: Raw input is parsed and validated using Standard Schema.
    4. Validated Middleware: If validation succeeds, the .useValidated() middleware stack runs with access to the typed parsedInput and accumulated context (ctx).
    5. Server Code: The core async action function executes using the validated input and context.
    6. Validate Output: If an outputSchema is 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.

  3. 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 useQuery for 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.
  4. Handle Next.js navigation functions in safe actions

    Next.js navigation functions such as redirect(), notFound(), forbidden(), and unauthorized() are supported within safe actions. next-safe-action automatically 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");
    	});
  5. 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.
  6. Narrow the result type using useAction status

    The useAction return object is a discriminated union. Checking the status or shorthand booleans (like hasSucceeded or hasErrored) narrows the result type, allowing safe access to data, serverError, or validationErrors without 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
    	}
    }
  7. Implement stateful form actions with useStateAction

    The useStateAction hook 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 enable onNavigation callbacks. 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>
    	);
    }
    
  8. Use useStateAction for stateful forms and serialized writes

    The useStateAction hook is used for actions defined with .stateAction(). It is built on React's useActionState and 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: useStateAction requires JavaScript and does not support no-JS progressive enhancement. For no-JS support, use React's useActionState directly.

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