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.