Next.js Server Actions
Answer
Server Actions are async functions that run on the server, allowing you to handle form submissions and data mutations without creating separate API routes.
Basic Server Action
// app/actions.js
"use server";
export async function createUser(formData) {
const name = formData.get("name");
const email = formData.get("email");
await db.users.create({ name, email });
revalidatePath("/users");
}
// app/page.jsx
import { createUser } from "./actions";
export default function Page() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit">Create User</button>
</form>
);
}
Inline vs Module Actions
// Module-level (reusable)
// app/actions.js
"use server";
export async function submitForm(formData) {
/* ... */
}
// Inline (component-specific)
export default function Page() {
async function handleSubmit(formData) {
"use server";
// Server code here
}
return <form action={handleSubmit}>...</form>;
}
With Loading States
"use client";
import { useFormStatus } from "react-dom";
import { createPost } from "./actions";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Creating..." : "Create Post"}
</button>
);
}
export default function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<SubmitButton />
</form>
);
}
With useActionState
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
const initialState = { message: null, errors: {} };
export default function Form() {
const [state, formAction, isPending] = useActionState(
createUser,
initialState
);
return (
<form action={formAction}>
<input name="email" />
{state.errors?.email && <p>{state.errors.email}</p>}
<button disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
// actions.js
"use server";
export async function createUser(prevState, formData) {
const email = formData.get("email");
if (!email.includes("@")) {
return {
errors: { email: "Invalid email" },
message: null,
};
}
await db.users.create({ email });
return {
errors: {},
message: "User created successfully!",
};
}
Revalidation
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function createPost(formData) {
await db.posts.create({ ... });
// Revalidate specific path
revalidatePath('/posts');
// Revalidate by tag
revalidateTag('posts');
}
// In fetch with tag
async function getPosts() {
const res = await fetch('/api/posts', {
next: { tags: ['posts'] }
});
return res.json();
}
Redirects
'use server';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
const post = await db.posts.create({ ... });
redirect(`/posts/${post.id}`);
}
With JavaScript-Triggered Actions
"use client";
import { deletePost } from "./actions";
export default function DeleteButton({ postId }) {
const handleDelete = async () => {
if (confirm("Delete this post?")) {
await deletePost(postId);
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Error Handling
'use server';
export async function createUser(formData) {
try {
await db.users.create({ ... });
return { success: true };
} catch (error) {
// Return error - don't throw
return {
success: false,
error: error.message
};
}
}
Optimistic Updates
"use client";
import { useOptimistic } from "react";
import { addTodo } from "./actions";
export default function Todos({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
const title = formData.get("title");
addOptimisticTodo({ title, id: Date.now() });
await addTodo(formData);
}
return (
<>
{optimisticTodos.map((todo) => (
<div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</div>
))}
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Add</button>
</form>
</>
);
}
Key Points
'use server'directive marks server-side functions- Use with forms via
actionattribute useFormStatusfor loading statesuseActionStatefor form state and errorsrevalidatePath/revalidateTagto update cache- Works without JavaScript (progressive enhancement)
- Can call from client components via import