← Back to Docs
Recipes
Form Actions
Server-side form handling with Next.js Server Actions — no client JavaScript required.
Basic Pattern
// app/actions.ts
'use server'
export async function submitForm(
prevState: { message: string },
formData: FormData
) {
const name = formData.get('name')
if (!name) return { message: 'Name required' }
// persist to database
return { message: 'Saved' }
}Client Component
'use client'
import { useFormState } from 'react-dom'
import { submitForm } from '@/app/actions'
export function Form() {
const [state, action] = useFormState(submitForm, {
message: '',
})
return (
<form action={action}>
<input name="name" />
<p>{state.message}</p>
<button type="submit">Save</button>
</form>
)
}Progressive Enhancement
Forms work without JavaScript. Add useFormStatus for loading states when JS is available.
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
)
}Validation with Zod
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
export async function submit(
prev: { errors?: string[] },
fd: FormData
) {
const parsed = schema.safeParse(
Object.fromEntries(fd)
)
if (!parsed.success) {
return { errors: parsed.error.issues
.map(i => i.message) }
}
return { errors: [] }
}Tip: Server Actions run sequentially per form. For parallel mutations, call them from event handlers instead of the form action prop.