SyntaxStudy
Sign Up
Next.js Mutations with Server Actions
Next.js Beginner 1 min read

Mutations with Server Actions

Server Actions are async functions that run on the server and can be called directly from Client Components or form elements. They are defined by adding the 'use server' directive inside the function body (for inline actions) or at the top of a file (for module-level actions). Server Actions enable form submissions, data mutations, and server-side logic without writing a separate API endpoint. Forms in Next.js can trigger Server Actions via the action prop. When a form is submitted, Next.js serialises the FormData and sends it to the Server Action. Because Server Actions run on the server, they can safely access databases, environment variables, and other server-only resources. After a mutation, you typically call revalidatePath() to purge the relevant cache and trigger a re-render of the affected page. The useFormStatus hook from react-dom gives you the pending state of the nearest parent form so you can show a loading indicator. The useFormState hook (also from react-dom) lets you pass state from a Server Action back to the client — useful for returning validation errors. Together they provide a complete progressively-enhanced form pattern that works even without JavaScript.
Example
// app/todos/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function createTodo(formData: FormData) {
  const title = formData.get('title') as string;

  if (!title || title.trim().length === 0) {
    return { error: 'Title is required' };
  }

  await db.todo.create({ data: { title: title.trim() } });

  revalidatePath('/todos'); // refresh the todos list
  return { success: true };
}

export async function deleteTodo(id: string) {
  await db.todo.delete({ where: { id } });
  revalidatePath('/todos');
}

// app/todos/NewTodoForm.tsx
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { createTodo } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Adding...' : 'Add Todo'}
    </button>
  );
}

export default function NewTodoForm() {
  const [state, formAction] = useFormState(createTodo, null);

  return (
    <form action={formAction}>
      <input name="title" type="text" placeholder="New todo..." />
      {state?.error && <p className="text-red-500">{state.error}</p>}
      <SubmitButton />
    </form>
  );
}