Build forms in React using useActionState and Server Actions.
In this guide, we will take a look at building forms with Next.js using useActionState and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more.
We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state.
Bug Report
Help us improve by reporting bugs you encounter.
"use client"import * as React from "react"
Note: The examples on this page intentionally disable browser validation
to show how schema validation and form errors work in server actions.
This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the <Field /> component, which gives you complete flexibility over the markup and styling.
Uses Next.js <Form /> component for navigation and progressive enhancement.
<Field /> components for building accessible forms.
useActionState for managing form state and errors.
We'll start by defining the shape of our form using a Zod schema in a schema.ts file.
Note: This example uses zod v3 for schema validation, but you can
replace it with any other schema validation library. Make sure your schema
library conforms to the Standard Schema specification.
schema.ts
import { z } from "zod"export const formSchema = z.object({ title: z .string() .min(5, "Bug title must be at least 5 characters.") .max(32, "Bug title must be at most 32 characters."), description: z .string() .min(20, "Description must be at least 20 characters.") .max(100, "Description must be at most 100 characters."),})
Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server.
schema.ts
import { z } from "zod"export type FormState = { values?: z.infer<typeof formSchema> errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>> success: boolean}
Important: We define the schema and the FormState type in a separate file so we can import them into both the client and server components.
A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state.
actions.ts
"use server"import { formSchema, type FormState } from "./form-next-demo-schema"export async function demoFormAction( _prevState: FormState, formData: FormData) { const values = { title: formData.get("title") as string, description: formData.get("description") as string, } const result = formSchema.safeParse(values) if (!result.success) { return { values, success: false, errors: result.error.flatten().fieldErrors, } } // Do something with the values. // Call your database or API here. return { values: { title: "", description: "", }, errors: null, success: true, }}
Note: We're returning values for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form.
That's it. You now have a fully accessible form with client and server-side validation.
When you submit the form, the formAction function will be called on the server. The server action will validate the form data and update the form state.
If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state.
Display errors next to the field using <FieldError />. Make sure to add the data-invalid prop to the <Field /> component and aria-invalid prop to the input.
To reset the form on success, you can omit the values from the server action and React will automatically reset the form state to the initial values. This is standard React behavior.
actions.ts
export async function demoFormAction( _prevState: FormState, formData: FormData) { const values = { title: formData.get("title") as string, description: formData.get("description") as string, } // Validation. if (!result.success) { return { values, success: false, errors: result.error.flatten().fieldErrors, } } // Business logic. callYourDatabaseOrAPI(values) // Omit the values on success to reset the form state. return { errors: null, success: true, }}
To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input.
actions.ts
export async function demoFormAction( _prevState: FormState, formData: FormData) { const values = { title: formData.get("title") as string, description: formData.get("description") as string, } // Validation. if (!result.success) { return { // Return the values on validation errors. values, success: false, errors: result.error.flatten().fieldErrors, } }}