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