C.W.K.
Stream
Lesson 02 of 08 · published

Form Actions

~20 min · form, action, FormData

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The action prop on <form>

The simplest form pattern: pass a Server Action to the form's action prop. Submission triggers the action with the form's FormData. The page re-renders with whatever revalidatePath invalidated.

Reading FormData

FormData entries are strings (or files). Use formData.get('name') as string for typed access. For files, type-narrow with formData.get('avatar') as File.

Passing extra arguments with .bind()

You often want to pass an id or context that's not in the form. Use .bind(null, id) — the bound argument arrives before the FormData. The framework safely encrypts the bound value before sending the action reference to the client.

Code

Form + Server Action·tsx
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function addTodo(formData: FormData) {
  const text = formData.get('text') as string;
  await db.todo.create({ data: { text, done: false } });
  revalidatePath('/todos');
}

// app/todos/page.tsx
import { addTodo } from '@/app/actions';

export default async function TodosPage() {
  const todos = await db.todo.findMany();
  return (
    <>
      <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
      <form action={addTodo}>
        <input name="text" required />
        <button>Add</button>
      </form>
    </>
  );
}
Bind extra arguments·tsx
'use server';
export async function updateTodo(id: string, formData: FormData) {
  const text = formData.get('text') as string;
  await db.todo.update({ where: { id }, data: { text } });
  revalidatePath('/todos');
}

function TodoItem({ todo }: { todo: { id: string; text: string } }) {
  const updateWithId = updateTodo.bind(null, todo.id);
  return (
    <form action={updateWithId}>
      <input name="text" defaultValue={todo.text} />
      <button>Save</button>
    </form>
  );
}

External links

Exercise

Build a tiny todo app with one Server Action for create and one for delete. Use .bind to pass the todo id to the delete action.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.