Death to the Internal API Route
I remember the tedious workflow of forms in Next.js 12.
You had to build an HTML form, attach an onSubmit handler, call e.preventDefault(), extract all the values, JSON.stringify them, execute a fetch request to /api/submit-form, handle the network response, manually set loading states, and then manually tell the router to refresh the page.
It was exhausting. Server Actions fixed this completely.
What is a Server Action?
Server Actions allow you to write asynchronous functions that run strictly on the server, but you can call them seamlessly from your client-side React components without any API routes.
// Explicitly declare this runs only on the server
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
// This function acts as the POST endpoint
export async function createPost(formData: FormData) {
const title = formData.get("title");
const content = formData.get("content");
await db.post.create({
data: { title: title as string, content: content as string },
});
// Purge the cache!
revalidatePath("/blog");
}Using it Without JavaScript!
The most beautiful thing about Server Actions is progressive enhancement. You can pass the function directly to the action prop of an HTML <form>.
import { createPost } from "./actions";
export default function NewPostForm() {
return (
// If the user disables Javascript, THIS STILL WORKS!
<form action={createPost}>
<input type="text" name="title" />
<textarea name="content" />
<button type="submit">Submit</button>
</form>
);
}Next.js handles the native form submission under the hood.
Handling Loading States (useActionState and useFormStatus)
Of course, we want rich interactivity. We want the submit button to spin and disable while the action is processing. React introduced hooks specifically for this paradigm.
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Saving..." : "Publish Post"}
</button>
);
}Then, you just drop this <SubmitButton /> inside your <form>. It automatically hooks into the context of the Server Action processing above it!
Error Handling Pattern
Server Actions should return predictable objects, just like a good API endpoint.
export async function createPost(formData: FormData) {
try {
// Validate data here...
await insertDB();
return { success: true };
} catch (error) {
return { success: false, error: "Database error occurred" };
}
}Server Actions feel like PHP or Rails all over again, but highly typed, strongly integrated into React, and offering an incredibly polished user experience. Embrace them.