the easy guide to nextjs server actions - By Sourav Mishra (@souravvmishra)
learn how to use server actions for forms, validation, and updating data without api routes.
server actions are probably the best thing added to next.js. you can just run server code right from your buttons without making annoying api routes.
i'll walk you through how i use them every day.
new to this? read my post on why server actions first if you're confused.
what are they?
a server function is just an async function that runs on the server. when you call it from the client, next.js handles the network stuff automatically.
we call them server actions when they change data.
making server functions
just use the "use server" string. you can put it at the top of a file or inside a function.
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title");
// do database stuff...
}
you can also just toss them right into server components.
export default function Page() {
async function createPost(formData: FormData) {
"use server";
// runs on server
}
return <form action={createPost}>{/* inputs */}</form>;
}
fun fact: forms work even if the user has javascript disabled.
using them in client components
you can't write them inside client components, but you can import them!
// components/create-button.tsx
"use client";
import { createPost } from "@/app/actions";
export function CreateButton() {
return <button formAction={createPost}>create</button>;
}
you can also just call them on clicks.
"use client";
import { incrementLike } from "./actions";
import { useState } from "react";
export function LikeButton() {
const [likes, setLikes] = useState(0);
return (
<button onClick={async () => setLikes(await incrementLike())}>
❤️ {likes}
</button>
);
}
showing loading states
use useActionState to show a loading spinner while it runs.
"use client";
import { useActionState } from "react";
import { createPost } from "@/app/actions";
export function CreateButton() {
const [state, action, pending] = useActionState(createPost, null);
return (
<button onClick={action} disabled={pending}>
{pending ? "loading..." : "create"}
</button>
);
}
refreshing data
when you change data, tell next.js to clear the cache using revalidatePath.
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
await db.posts.create({ title: formData.get("title") });
revalidatePath("/posts"); // updates the page instantly!
}
if you need to redirect the user, just do it after.
import { redirect } from "next/navigation";
export async function createPost() {
// save stuff
revalidatePath("/posts");
redirect(`/posts`);
}
error handling
never trust client data. always use something like zod to check it.
"use server";
import { z } from "zod";
const schema = z.object({
title: z.string().min(1),
});
export async function createPost(formData: FormData) {
const result = schema.safeParse({ title: formData.get("title") });
if (!result.success) {
return { error: "bad input bro" };
}
// save to db
}
what to remember
- use
"use server": it's magic. - forms work without js: super cool.
- handle loading: use
useActionState. - validate everything: seriously.
- refresh your data:
revalidatePathis your friend.
server actions make life so much easier. no more messy /api/ folders just to submit a form.
written by sourav mishra, probably writing another form right now.