Server Actions & Mutations

In the past, submitting a form to a database required writing an API route, creating a Client Component, hooking up `useEffect` or `onSubmit`, using `fetch()`, handling JSON, and dealing with loading states. With Next.js 14 Server Actions, you can literally call a server function directly from your form's action attribute.

Creating a Server Action

A server action is simply an async function marked with the 'use server' directive. This tells Next.js to expose this function as a secure API endpoint behind the scenes automatically via POST.

// app/actions.js
'use server'; // Marks all exports as server actions

export async function createPost(formData) {
  // formData is standard web FormData object!
  const title = formData.get('title');
  const content = formData.get('content');

  // Direct database insertion - SECURE!
  await db.posts.insert({ title, content });

  // Do not return secret info to the client here
}

Calling Server Actions in Forms

You can import this action and pass it into the action prop of an HTML form. Even if the user has JavaScript completely disabled in their browser, this form submission will successfully execute on the server!

import { createPost } from './actions';

// This is a Server Component! No 'use client' needed.
export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input type="text" name="title" placeholder="Post Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Submit Post</button>
    </form>
  )
}

Client-Side Enhancements (`useActionState` and `useFormStatus`)

While the form works without JS, modern users expect loading spinners and error messages. For these, you can combine Server Actions with React's new hooks inside a Client Component.

'use client';
import { useFormStatus } from 'react-dom';
import { createPost } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus(); // Automatically tracks the parent form's submission state
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving...' : 'Save Post'}
    </button>
  );
}

export default function EnhancedForm() {
  return (
    <form action={createPost}>
      <input type="text" name="title" />
      <SubmitButton />
    </form>
  );
}

Conclusion

Server Actions drastically cut down boilerplate code. You no longer need to write a standalone `/api/createPost` route just to handle a basic form. Next, we look at how to handle loading UIs flawlessly using Suspense & Streaming.