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 afetchrequest, 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.
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.
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:
useFormStatusmust 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.
- Authentication: Always check if the user is authorized.
- 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
}
});
}