Forms with Server Actions
onSubmit handler that calls preventDefault and manually reads input values, you pass the Server Action straight to the form's action prop: <form action={myServerAction}>. The browser's native form submission mechanism does the rest.The action receives a FormData object
FormData object containing every named field in the form. There's no manual parsing of a request body — extract each field with formData.get('fieldName'), where 'fieldName' matches the input's name attribute.Worked example: a comment form
Here's a simple comment form that validates its input and saves it via a Server Action, entirely on the server.
app/actions/comments.ts
'use server'
export async function addComment(formData: FormData) {
const author = formData.get('author')
const body = formData.get('body')
if (typeof author !== 'string' || author.trim() === '') {
throw new Error('Author is required')
}
if (typeof body !== 'string' || body.trim() === '') {
throw new Error('Comment cannot be empty')
}
// ...save { author, body } to the database...
}app/posts/[slug]/CommentForm.tsx
import { addComment } from '@/app/actions/comments'
export default function CommentForm() {
return (
<form action={addComment}>
<input name="author" placeholder="Your name" required />
<textarea name="body" placeholder="Your comment" required />
<button type="submit">Post comment</button>
</form>
)
}CommentForm has no 'use client' directive — it's a plain Server Component. Passing a Server Action to action works whether the form itself is rendered on the server or the client.<form action={...}> is built on the browser's native form submission behavior, the comment form above posts successfully even if the visitor has JavaScript turned off, or the bundle hasn't finished loading yet. The browser submits the form as a normal navigation, Next.js intercepts it on the server, runs the action, and re-renders the page with the result. This is progressive enhancement — the base case works without client-side JavaScript, and JavaScript (when available) upgrades the experience with things like pending states and inline error messages, covered on the next page.Extracting multiple fields and types
FormData values are always strings (or File objects for file inputs) — even checkboxes and numbers come through as strings, so convert them explicitly.Extracting a checkbox and a number
'use server'
export async function updateSettings(formData: FormData) {
const displayName = formData.get('displayName') as string
const isPublic = formData.get('isPublic') === 'on'
const age = Number(formData.get('age'))
// ...persist the parsed values...
}Pass a Server Action directly to a form:
<form action={myAction}>— noonSubmithandler orpreventDefaultneeded.The action receives a
FormDataobject as its only argument; read fields withformData.get('fieldName').Every
FormDatavalue is a string (orFile) — convert booleans and numbers explicitly.Because it rides on native form submission, this pattern works even with JavaScript disabled — a real progressive-enhancement win.
Always validate fields again on the server; client-side validation can always be bypassed.