Skip to main content

Form actions

You can utilize Server Actions as form actions too. next-safe-action allows you to define stateful or stateless form actions.

FormData input

For defining actions with FormData input, the recommended approach is to use the zod-form-data library, which allows you to do that. In these two examples below we'll be using it.

Stateless form actions

You can define a stateless safe action using the action instance method, and then pass it to the action prop of a form using direct execution, useAction hook or useOptimisticAction hook.

With this method, you can access previous result from the client component, both by awaiting the safe action or by using the result prop returned by the hooks. You can't access previous result on the server, though, and this is why this approach is called stateless: the server doesn't know the previous result of the action execution.

Here's an example using the useAction hook:

stateless-form-action.ts
"use server";

import { actionClient } from "@/lib/safe-action";
import { z } from "zod";
import { zfd } from "zod-form-data";

const schema = zfd.formData({
name: zfd.text(z.string().min(1).max(20)),
});

export const statelessFormAction = actionClient
.schema(schema)
.action(async ({ parsedInput }) => {
await new Promise((res) => setTimeout(res, 1000));

return {
newName: parsedInput.name,
};
});
stateless-form.tsx
"use client";

import { useAction } from "next-safe-action/hooks";
import { statelessFormAction } from "./stateless-form-action";

export default function StatelessForm() {
const { execute } = useAction(statelessFormAction);

return (
<form action={execute}>
<input type="text" name="name" placeholder="Name" />
<button type="submit">Submit</button>
</form>
);
}

You can also find this example in the playground app: stateless form action example.

Stateful form actions

You can define a stateful safe action using the stateAction instance method, and then pass it to the action prop of a form using the useStateAction hook.

With this method, you can access previous result both from the client component, by using the result prop returned by the hook, and on the server, where you define the action. More information about that in the stateAction and useStateAction sections.

Note that if you want or need to use stateful actions:

  1. You must define them with stateAction instance method. This changes the signature of the Server Action function, placing the prevResult as the first argument.
  2. If you're on Next.js < 15, you can manually pass them to useFormState hook, which will be deprecated.
  3. Starting from Next.js 15, you should use the built-in useStateAction hook (which uses React's useActionState hook under the hood) exported from next-safe-action/stateful-hooks path.

Here's an example of a stateful action, using the useStateAction hook:

stateful-form-action.ts
"use server";

import { action } from "@/lib/safe-action";
import { z } from "zod";
import { zfd } from "zod-form-data";

const schema = zfd.formData({
name: zfd.text(z.string().min(1).max(20)),
});

// Note that we need to explicitly give a type to `stateAction` here, for its return object.
// This is because TypeScript can't infer the return type of the function and then "pass it" to
// the second argument of the server code function (`prevResult`). If you don't need to access `prevResult`,
// though, you can omit the type here, since it will be inferred just like with `action` method.
export const statefulFormAction = action
.schema(schema)
.stateAction<{
prevName?: string;
newName: string;
}>(async ({ parsedInput }, { prevResult }) => {
return {
prevName: prevResult.data?.newName,
newName: parsedInput.name,
};
});
stateful-form.tsx
"use client";

import { useStateAction } from "next-safe-action/stateful-hooks";
import { statefulFormAction } from "./stateful-form-action";

export default function StatefulForm() {
const { execute } = useStateAction(
statefulFormAction,
{
initResult: { data: { newName: "jane" } }, // optionally pass initial state
}
);

return (
<form action={execute}>
<input type="text" name="name" placeholder="Name" />
<button type="submit">Submit</button>
</form>
);
}

You can also find this example in the playground app: stateful form action example.