Next.js form approach with useActionState and Server Actions
Next.js forms can be built using the <Form /> component for navigation and progressive enhancement, <Field /> components for building accessible forms, useActionState for managing form state and errors, Server Actions for handling form submissions, and Zod for server-side validation.
FormState type definition for Next.js forms
The FormState type should include three properties: values (optional, inferred from the Zod schema), errors (null or a Partial Record mapping field names to arrays of error strings), and success (boolean). This type is used to type the form state on both client and server components.
Basic form anatomy with Field component
A basic form structure uses <Form action={formAction}>, <FieldGroup>, <Field data-invalid={condition}>, <FieldLabel>, <Input>, <FieldDescription>, <FieldError>, and a submit <Button>. The Field component provides complete flexibility over markup and styling.
Server action validation with safeParse
Use safeParse() on the Zod schema in the server action to validate form data. When validation fails, return the values, success set to false, and errors from result.error.flatten().fieldErrors. When validation succeeds, return errors as null and success as true.
Return values on validation errors to preserve user input
When the server action validation fails, return the values in the response so the form state maintains the user's input. This prevents the form from being reset and allows the user to see what they entered alongside the error messages.
Reset form on successful submission
To reset the form on successful submission, omit the values from the server action return. React will automatically reset the form state to initial values. Only return errors as null and success as true.
Using pending state for loading indicators and disabled fields
The pending prop returned from useActionState indicates whether the form is currently submitting. Use it to disable form inputs with the disabled prop, disable the submit button, and show loading indicators like spinners. Apply data-disabled prop to <Field /> components for styling disabled state.
Displaying form field errors
To display field errors, add data-invalid prop to the <Field /> component and aria-invalid prop to the input element. Render <FieldError /> conditionally when errors exist for that field, displaying the first error message from the errors array.
Business logic validation in server actions
After schema validation succeeds, you can add custom business logic validation in the server action, such as checking if an email already exists in the database. Return values, success set to false, and a custom errors object if business logic validation fails.
useActionState hook initialization with default state
Initialize useActionState with three arguments: the server action function, and an initial form state object with properties errors (typically null), and success (typically false). useActionState returns formState, formAction, and pending.
Zod schema validation with min/max constraints
Use Zod to define form schemas with validation rules. For strings, use .min(length, message) for minimum character length and .max(length, message) for maximum character length. Error messages are returned in validation errors.
Schema and FormState must be in separate file for client and server use
Define the Zod schema and FormState type in a separate file that can be imported into both client and server components. This allows both the client and server to reference the same schema and type definitions.
Disable Field component using data-disabled prop
To apply disabled state styling to a <Field /> component, use the data-disabled prop and pass the pending value. Also set disabled={pending} on the input element inside the field.
Disable submit button during form submission
Use disabled={pending} on the submit button to prevent multiple submissions while the form is processing. Optionally display a spinner when pending is true.
TanStack Form overview
TanStack Form is a headless form handling library used to build forms in React. It provides the useForm hook for form state management, the form.Field component with a render prop pattern for controlled inputs, client-side validation using Zod, and real-time validation feedback.
useForm hook setup with Zod validation
Use the useForm hook from TanStack Form to create a form instance. Pass defaultValues (an object with initial form field values), validators (with onSubmit, onChange, and/or onBlur modes), and onSubmit (an async function called with the validated form data). Example: const form = useForm({ defaultValues: { title: "", description: "" }, validators: { onSubmit: formSchema }, onSubmit: async ({ value }) => { toast.success("Form submitted successfully") } })
TanStack Form validation modes
TanStack Form supports three validation modes via the validators option: "onChange" (validation triggers on every change), "onBlur" (validation triggers on blur), and "onSubmit" (validation triggers on submit). Multiple modes can be configured simultaneously by providing schemas for multiple modes in the validators object.
form.Field component for controlled inputs
The form.Field component from TanStack Form accepts a name prop and a children render prop that receives a field object. The field object contains state (with meta.isTouched, meta.isValid, value), handleBlur, and handleChange methods. This render prop pattern gives complete control over markup and styling.
Accessibility for invalid form fields
To make invalid fields accessible: add the data-invalid prop to the Field component with the value field.state.meta.isTouched && !field.state.meta.isValid, and add the aria-invalid prop to the form control (Input, SelectTrigger, Checkbox, etc.) with the same value.
Input field pattern with TanStack Form
For input fields: use field.state.value and field.handleChange on the Input component. Set id, name, onBlur, onChange, and aria-invalid props. Add data-invalid to the Field wrapper and display errors with FieldError when isInvalid is true.
Textarea field pattern with TanStack Form
For textarea fields: use field.state.value and field.handleChange on the Textarea component. Set id, name, onBlur, onChange, and aria-invalid props. Add data-invalid to the Field wrapper and display errors with FieldError when isInvalid is true.
Select field pattern with TanStack Form
For select components: use field.state.value and field.handleChange (via onValueChange) on the Select component. Set aria-invalid on the SelectTrigger component and data-invalid on the Field wrapper. Display errors with FieldError when isInvalid is true.
Checkbox field pattern with TanStack Form
For checkbox fields: use field.state.value and field.handleChange on the Checkbox component. Set aria-invalid on Checkbox and data-invalid on the Field wrapper. For checkbox arrays, use mode="array" on form.Field and add data-slot="checkbox-group" to the FieldGroup for proper styling. Use field.pushValue() to add and field.removeValue(index) to remove items.
Radio group field pattern with TanStack Form
For radio groups: use field.state.value and field.handleChange (via onValueChange) on the RadioGroup component. Set aria-invalid on RadioGroupItem components and data-invalid on the Field wrapper. Display errors with FieldError when isInvalid is true.
Switch field pattern with TanStack Form
For switches: use field.state.value and field.handleChange (via onCheckedChange) on the Switch component. Set aria-invalid on Switch and data-invalid on the Field wrapper. Display errors with FieldError when isInvalid is true.
Reset form to default values
Use form.reset() to reset the form to its default values. This is typically called in an onClick handler on a button element.
Array field management with TanStack Form
Use mode="array" on the form.Field component to enable array field management. Access individual array items using bracket notation: fieldName[index].propertyName in nested form.Field components. Use field.pushValue(item) to add items and field.removeValue(index) to remove items.
Array field validation with Zod
Validate array fields using Zod's array methods. Example: z.object({ emails: z.array(z.object({ address: z.string().email("Enter a valid email address.") })).min(1, "Add at least one email address.").max(5, "You can add up to 5 email addresses.") })
Form submission with TanStack Form
Wrap form submission by calling e.preventDefault() and then form.handleSubmit() in the form's onSubmit handler. TanStack Form will validate the form data before calling the onSubmit callback function defined in useForm.