Skip to main content

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 action attribute
  • useFormStatus for loading states
  • useActionState for form state and errors
  • revalidatePath/revalidateTag to update cache
  • Works without JavaScript (progressive enhancement)
  • Can call from client components via import