Server Actions: Mutations Without API Routes
Server Actions let you run server-side code directly from your components. No API route needed. Just mark a function with 'use server' and call it from a form or button. It's like magic, but it's actually just an RPC call.
Basic Server Action
src/app/contact/actions.ts
tsx
1// src/app/contact/actions.ts2"use server";34import { db } from "@/lib/database";56export async function submitContact(formData: FormData) {7const name = formData.get("name") as string;8const email = formData.get("email") as string;9const message = formData.get("message") as string;1011await db.insert("messages", { name, email, message });1213// You can also revalidate cached data14// revalidatePath("/messages");15}src/app/contact/page.tsx
tsx
1// src/app/contact/page.tsx2import { submitContact } from "./actions";34export default function ContactPage() {5return (6 <form action={submitContact}>7 <input name="name" placeholder="Name" required />8 <input name="email" type="email" placeholder="Email" required />9 <textarea name="message" placeholder="Message" required />10 <button type="submit">Send Message</button>11 </form>12);13}š”
How it works under the hood
When the form submits, Next.js sends a POST request to the server, executes your function, and returns the result. No fetch calls, no API routes, no CORS. It works even with JavaScript disabled (progressive enhancement!).
With Validation & Feedback
src/app/contact/actions.ts
tsx
1// src/app/contact/actions.ts2"use server";34interface ActionState {5success: boolean;6message: string;7}89export async function submitContact(10prevState: ActionState,11formData: FormData12): Promise<ActionState> {13const email = formData.get("email") as string;1415if (!email.includes("@")) {16 return { success: false, message: "Invalid email address" };17}1819// Save to database...2021return { success: true, message: "Message sent!" };22}src/app/contact/ContactForm.tsx
tsx
1"use client";23import { useActionState } from "react";4import { submitContact } from "./actions";56export default function ContactForm() {7const [state, action, isPending] = useActionState(submitContact, {8 success: false,9 message: "",10});1112return (13 <form action={action}>14 <input name="email" type="email" placeholder="Email" required />15 <button type="submit" disabled={isPending}>16 {isPending ? "Sending..." : "Send"}17 </button>18 {state.message && (19 <p className={state.success ? "text-green-600" : "text-red-600"}>20 {state.message}21 </p>22 )}23 </form>24);25}š
useActionState is the new way
React 19 introduced useActionState (replacing useFormState). It gives you:
⢠The previous state
⢠The action function to pass to your form
⢠An isPending boolean for loading states
Use it for any form that needs loading states or server validation.
⢠The previous state
⢠The action function to pass to your form
⢠An isPending boolean for loading states
Use it for any form that needs loading states or server validation.