Server Actions

Server Actions are asynchronous functions that are executed on the server. They can be called from Server Components directly or from Client Components (typically via form submissions).

[!NOTE] The Mental Model: Think of Server Actions as Remote Procedure Calls (RPC) built directly into React. Instead of manually creating an API endpoint (/api/update-user), defining a fetch request, handling headers, and parsing JSON, you just write a function and call it. The compiler handles the network plumbing for you.

1. The ‘use server’ Directive

To define a Server Action, you use the 'use server' directive. This tells the compiler to create an HTTP endpoint for this function behind the scenes.

1. Inside a Server Component

You can define an action directly inside a Server Component function.


// app/page.tsx
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export default function Page() {
  // Server Action
  async function createPost(formData: FormData) {
    'use server'; // Marks this function as an action

    const title = formData.get('title') as string;
    await db.post.create({ data: { title } });
    revalidatePath('/'); // Refresh the data
  }

  return (
    <form action={createPost}>
      <input name="title" className="border p-2 rounded" />
      <button type="submit">Create</button>
    </form>
  );
}

2. In a Separate File (Reusable)

If you need to use an action inside a Client Component, you must define it in a separate file with 'use server' at the top.


// app/actions.ts
'use server';

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

export async function likePost(postId: string) {
  await db.like.create({ data: { postId } });
  revalidatePath(`/posts/${postId}`);
}

Then import it:


// app/like-button.tsx
'use client';
import { likePost } from './actions';

export function LikeButton({ id }: { id: string }) {
  return (
    <button onClick={() => likePost(id)}>
      Like
    </button>
  );
}

2. Action Lifecycle Simulator

Visualize the journey of a Server Action from the user’s click to the server and back. Watch how the request is serialized, processed, and how the UI updates.

💻
Client
POST
☁️
Server
RSC
🎨
UI Update
Ready...

3. Optimistic Updates

Waiting for the server to respond can feel slow. Users expect instant feedback. useOptimistic allows you to update the UI immediately while the server action runs in the background.

🤖
Assistant
Online
Hello! Send a message to test Optimistic UI.

Implementing useOptimistic


// app/chat.tsx
'use client';

import { useOptimistic, useRef } from 'react';
import { sendMessage } from './actions';

export function Chat({ messages }: { messages: Message[] }) {
  const formRef = useRef<HTMLFormElement>(null);

  // Define optimistic state
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage: string) => [
      ...state,
      { id: Math.random(), text: newMessage, sending: true }
    ]
  );

  async function action(formData: FormData) {
    const text = formData.get('message') as string;

    // 1. Update UI immediately
    addOptimisticMessage(text);

    // 2. Reset form
    formRef.current?.reset();

    // 3. Call server (background)
    await sendMessage(text);
  }

  return (
    <div>
      {optimisticMessages.map((m) => (
        <div key={m.id} className={m.sending ? 'opacity-50' : ''}>
          {m.text} {m.sending && '(Sending...)'}
        </div>
      ))}

      <form action={action} ref={formRef}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

4. Pending States with useFormStatus

When a form is submitting, you often want to disable the submit button or show a loading spinner. The useFormStatus hook provides this information.

[!IMPORTANT] Component Boundary: useFormStatus must be used in a component rendered inside the <form>. It will not work if used in the same component that renders the <form>.


// app/submit-button.tsx
'use client';
import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving...' : 'Save'}
    </button>
  );
}


// app/page.tsx
import { SubmitButton } from './submit-button';

export default function Page() {
  return (
    <form action={createPost}>
      <input name="title" />
      <SubmitButton /> {/* Works! */}
    </form>
  );
}

5. Security & Validation

Because Server Actions are public API endpoints, you must treat them with the same security scrutiny as a REST API.

  1. Authentication: Always check if the user is authorized.
  2. Validation: Always validate the input (e.g., using Zod).

// app/actions.ts
'use server';

import { z } from 'zod';
import { auth } from '@/auth';

const schema = z.object({
  title: z.string().min(1),
});

export async function createPost(formData: FormData) {
  // 1. Check Auth
  const session = await auth();
  if (!session) throw new Error("Unauthorized");

  // 2. Validate Input
  const parsed = schema.safeParse({
    title: formData.get('title'),
  });

  if (!parsed.success) {
    return { error: parsed.error.message };
  }

  // 3. Mutate Data
  await db.post.create({
    data: {
      title: parsed.data.title,
      userId: session.user.id
    }
  });
}