Forms with Server Actions
Wire a form's action prop to a Server Action to handle submissions with progressive enhancement.
Syntax
<form action={serverAction}>Passing a Server Action to a form's action prop is the idiomatic way to handle submissions. The form works even before JavaScript loads (progressive enhancement), and Next automatically sends the FormData to your action on the server.
Reading the data
Your action receives a FormData object. Use formData.get('fieldName') to read each input by its name.
Example
// app/actions.ts
'use server';
import { db } from './db';
export async function subscribe(formData: FormData) {
const email = formData.get('email') as string;
await db.subscriber.create({ data: { email } });
}
// app/page.tsx
import { subscribe } from './actions';
export default function Page() {
return (
<form action={subscribe}>
<input type="email" name="email" required />
<button>Subscribe</button>
</form>
);
}When to use it
- A newsletter signup form uses a Server Action so submissions work even before JavaScript loads on slow connections.
- A contact form passes a Server Action to the form's action prop to validate and save data entirely on the server.
- A multi-step wizard submits each step via a Server Action and redirects to the next step on success.
More examples
Form with action prop
Passing a Server Action to the form's action prop enables progressive enhancement with no client JavaScript required.
// app/contact/page.tsx
async function sendMessage(formData: FormData) {
'use server';
const email = formData.get('email') as string;
const message = formData.get('message') as string;
await saveContact({ email, message });
}
export default function ContactPage() {
return (
<form action={sendMessage}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}Validation inside the action
Zod validation runs server-side in the action, keeping schema logic away from client bundles.
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export async function sendMessage(formData: FormData) {
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) return { errors: result.error.flatten() };
await saveContact(result.data);
}Bind extra data to an action
bind pre-fills the first argument so the action receives the postId alongside the submitted FormData.
import { updatePost } from '@/app/actions';
export default function EditForm({ postId }: { postId: string }) {
const updateWithId = updatePost.bind(null, postId);
return (
<form action={updateWithId}>
<input name="title" />
<button type="submit">Save</button>
</form>
);
}
Discussion