Server Actions are a game-changer for Next.js, letting you write server-side code directly within your React components, blurring the lines between client and server in a way that feels almost magical.

Imagine a user submitting a form. Normally, this would involve a separate API route, fetching data on the client, sending it over, and then handling the response. With Server Actions, you can write a function directly in your page.tsx or layout.tsx that executes on the server when the form is submitted.

Let’s see it in action with a simple form example.

// app/page.tsx
import { redirect } from 'next/navigation';

async function createPost(formData: FormData) {
  'use server'; // This is the magic!

  const title = formData.get('title');
  const content = formData.get('content');

  // Simulate a database write
  console.log('Creating post:', { title, content });

  // Redirect to a success page after creation
  redirect('/posts/success');
}

export default function HomePage() {
  return (
    <div>
      <h1>Create New Post</h1>
      <form action={createPost}>
        <input type="text" name="title" placeholder="Title" required />
        <br />
        <textarea name="content" placeholder="Content" required></textarea>
        <br />
        <button type="submit">Create Post</button>
      </form>
    </div>
  );
}

When this form is submitted, the createPost function, marked with 'use server', is executed on the Vercel server. No client-side JavaScript is needed to handle the form submission; the browser simply sends the FormData to the server action. The redirect function then works seamlessly, navigating the user to the /posts/success page.

The core problem Server Actions solve is the friction of building full-stack applications with Next.js. Traditionally, you’d manage two distinct codebases (or at least two distinct concerns): client-side UI and server-side API endpoints. This often led to:

  • Boilerplate: Defining API routes, handling request/response objects, serializing/deserializing data.
  • Context Switching: Developers constantly shifting between client and server logic.
  • Data Fetching Complexity: Managing data fetching on the client and then passing it to server mutations.

Server Actions streamline this by allowing you to colocate your server-side mutations and data fetching logic directly within your React components. This means your page.tsx can contain not only the UI but also the server-side functions that interact with your data.

Internally, when you define a Server Action, Next.js generates a special route for it behind the scenes. When a form with an action pointing to a Server Action is submitted, or when you invoke a Server Action from client-side JavaScript using startTransition, Next.js intercepts this and sends the request to the appropriate server-side handler. The arguments passed to the Server Action are serialized and sent to the server, where the function is executed. The return value (or exceptions thrown) is then sent back to the client.

You have several levers to control their behavior and performance.

1. action vs. formAction: The action prop on a <form> element, when passed a Server Action function, will automatically stringify the form data and send it to the server. This is the most common and simplest way to use Server Actions for form mutations. The formAction prop on a <button> or <input type="submit"> allows you to specify a different Server Action to be executed when that specific element triggers the form submission. This is useful if you have multiple buttons in a form that should trigger different server-side logic.

2. bind for pre-filled arguments: You can use Function.prototype.bind to pre-fill arguments for your Server Actions. This is incredibly powerful for creating reusable action components. For example, you might have a deleteItem Server Action that takes an id. You can then bind specific IDs to create specialized delete buttons.

// app/items/[id]/page.tsx
import { deleteItem } from '@/actions'; // Assuming deleteItem is exported from actions.ts

async function ItemDetails({ params }: { params: { id: string } }) {
  const itemId = params.id;
  const handleDelete = deleteItem.bind(null, itemId); // Bind the itemId

  return (
    <div>
      <h1>Item: {itemId}</h1>
      <form action={handleDelete}>
        <button type="submit">Delete Item</button>
      </form>
    </div>
  );
}

Here, handleDelete is a new function that, when called, will invoke deleteItem with itemId already supplied as the first argument.

3. Progressive Enhancement: Server Actions are designed with progressive enhancement in mind. If JavaScript is disabled or fails to load, a form submission to a Server Action will behave like a traditional HTML form submission, resulting in a full page reload. This ensures your application remains functional even without client-side JavaScript.

4. Mutations vs. Non-mutations: Server Actions can be used for both mutations (modifying data) and non-mutations (fetching data). For mutations, Next.js automatically handles revalidating data caches after the action completes, ensuring your UI reflects the latest state. For non-mutations, you might want to manually revalidate or refetch data.

5. Caching and Revalidation: When a Server Action modifies data, Next.js can automatically revalidate relevant data caches. You can explicitly control this using revalidatePath and revalidateTag from next/cache. This is crucial for ensuring that subsequent data fetches reflect the changes made by your Server Actions.

A subtle but powerful aspect of Server Actions is their ability to handle non-form data directly from client-side JavaScript. You can import a Server Action into a client component and invoke it using startTransition for a seamless, client-side feel without the need for manual API route creation.

// app/client-component.tsx
'use client';

import { useState, useTransition } from 'react';
import { addTodo } from '@/actions'; // Assuming addTodo is a Server Action

export default function AddTodoForm() {
  const [text, setText] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    startTransition(() => {
      addTodo(text); // Invoke Server Action directly
      setText(''); // Clear input after submission
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        disabled={isPending}
      />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Todo'}
      </button>
    </form>
  );
}

This approach leverages the useTransition hook to provide immediate UI feedback while the Server Action runs in the background. The startTransition function ensures that the UI remains responsive during the server operation.

The next major hurdle you’ll encounter is managing complex state updates and optimistic UI patterns with Server Actions, especially when dealing with multiple, interdependent mutations.

Want structured learning?

Take the full Vercel course →