Server Actions
Mutate data from the server using "use server" functions -- no API routes needed.
Busque em todas as páginas da documentação
Mutate data from the server using "use server" functions -- no API routes needed.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida -- pronto para copiar e colar.
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.post.create({ data: { title } });
revalidatePath("/posts");
}// app/posts/new/page.tsx (Server Component)
import { createPost } from "@/app/actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}Quando usar isso: Você precisa criar, atualizar ou excluir dados a partir de um formulário ou clique de botão sem escrever uma rota de API dedicada.
// app/actions/todo.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
export async function addTodo(formData: FormData) {
const text = formData.get("text") as string;
if (!text || text.trim().length === 0) {
return { error: "Text is required" };
}
await db.todo.create({ data: { text: text.trim(), done: false } });
revalidatePath("/todos");
}
export async function toggleTodo(id: string) {
const todo = await db.todo.findUnique({ where: { id } });
if (!todo) return { error: "Not found" };
await db.todo.update({
where: { id },
data: { done: !todo.done },
});
revalidatePath("/todos");
}
export async function deleteTodo(id: string) {
await db.todo.delete({ where: { id } });
revalidatePath("/todos");
}// app/todos/page.tsx (Server Component)
import { db } from "@/lib/db";
import { addTodo } from "@/app/actions/todo";
import { TodoItem } from "./todo-item";
export default async function TodosPage() {
const todos = await db.todo.findMany({ orderBy: { createdAt: "desc" } });
return (
<main className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Todos</h1>
<form action={addTodo} className="flex gap-2 mb-6">
<input
name="text"
placeholder="What needs doing?"
className="flex-1 border rounded px-3 py-2"
required
/>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Add
</button>
</form>
<ul className="space-y-2">
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</main>
);
}// app/todos/todo-item.tsx
"use client";
import { useTransition } from "react";
import { toggleTodo, deleteTodo } from "@/app/actions/todo";
type Todo = { id: string; text: string; done: boolean };
export function TodoItem({ todo }: { todo: Todo }) {
const [isPending, startTransition] = useTransition();
return (
<li
className={`flex items-center gap-3 p-2 border rounded ${
isPending ? "opacity-50" : ""
}`}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => startTransition(() => toggleTodo(todo.id))}
/>
<span className={todo.done ? "line-through text-gray-400" : "flex-1"}>
{todo.text}
</span>
<button
onClick={() => startTransition(() => deleteTodo(todo.id))}
className="text-red-500 text-sm"
>
Delete
</button>
</li>
);
}O que isso demonstra:
"use server" separado<form action> para aprimoramento progressivouseTransition para UI pendenterevalidatePath para atualizar dados em cache após mutaçõesasync marcada com "use server" (no topo de um arquivo ou inline no corpo de uma função dentro de um Server Component).<form action>, o React envia os dados do formulário para o servidor e chama a função. Isso funciona mesmo sem JavaScript habilitado (aprimoramento progressivo).revalidatePath() ou revalidateTag() para invalidar dados em cache e disparar uma nova renderização.Server Action Inline (definida dentro de um Server Component):
export default function Page() {
async function handleSubmit(formData: FormData) {
"use server";
const email = formData.get("email") as string;
await subscribeToNewsletter(email);
}
return (
<form action={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
);
}Usando useActionState para estado e validação de formulário:
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions/user";
type State = { error?: string; success?: boolean };
export function SignupForm() {
const [state, formAction, isPending] = useActionState<State, FormData>(
createUser,
{ error: undefined, success: false }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
{state.error && <p className="text-red-500">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Sign Up"}
</button>
</form>
);
}Atualizações otimistas com useOptimistic:
"use client";
import { useOptimistic, useTransition } from "react";
import { addTodo } from "@/app/actions/todo";
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: crypto.randomUUID(), text: newTodo, done: false },
]
);
const [, startTransition] = useTransition();
return (
<form
action={(formData) => {
const text = formData.get("text") as string;
startTransition(async () => {
addOptimistic(text);
await addTodo(formData);
});
}}
>
<input name="text" required />
<button type="submit">Add</button>
<ul>
{optimisticTodos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
</form>
);
}// Assinatura da Server Action ao ser usada com <form action>
export async function myAction(formData: FormData): Promise<void> {
"use server";
// ...
}
// Server Action com useActionState -- recebe o estado anterior + formData
export async function myAction(
prevState: { error?: string },
formData: FormData
): Promise<{ error?: string }> {
"use server";
// ...
}
// Server Action chamada diretamente (não de um formulário)
export async function deleteItem(id: string): Promise<{ success: boolean }> {
"use server";
// ...
}Argumentos devem ser serializáveis -- Você não pode passar nós DOM, instâncias de classe ou funções para uma Server Action. Correção: Passe apenas strings, números, booleanos, FormData, objetos simples, arrays, Date, Map, Set ou arrays tipados.
revalidatePath não funciona dentro de try/catch com redirect -- Chamar redirect() lança um erro especial do Next.js. Se você o envolver em try/catch, o redirect será engolido. Correção: Chame redirect() fora do bloco try/catch, após toda a outra lógica.
Server Actions são apenas POST -- Elas não podem ser usadas para requisições GET ou leituras idempotentes. Correção: Use um fetch regular ou Server Component para ler dados.
Uploads de arquivos precisam de FormData -- Você não pode usar JSON.stringify para arquivos. Correção: Use um <form> com encType="multipart/form-data" ou construa FormData manualmente no cliente.
Sem acesso aos cabeçalhos da requisição por padrão -- Server Actions não recebem automaticamente cookies ou cabeçalhos. Correção: Use cookies() e headers() de next/headers dentro do corpo da action.
Variáveis de closure são serializadas -- Se você definir uma action inline "use server" que fecha sobre uma variável, essa variável é serializada e enviada com a action. Correção: Esteja ciente de dados sensíveis em closures; valide todas as entradas no servidor.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Route Handlers (API routes) | Você precisa de um endpoint REST para clientes externos ou webhooks | Você apenas muta dados da sua própria UI |
SWR mutate + API route | Você precisa de invalidação de cache instantânea no cliente com SWR | Você quer aprimoramento progressivo |
| tRPC | Você quer segurança de tipo ponta a ponta com uma camada RPC dedicada | Você quer manter as coisas nativas do framework |
fetch de Client Component | Você precisa de controle total sobre o manuseio de requisição/resposta | Server Actions já cobrem seu caso de uso |
<form action>, Server Actions suportam aprimoramento progressivoonClickredirect() lança um erro especial do Next.js internamente para disparar a navegaçãoredirect() fora do bloco try/catch, após toda a outra lógica"use client";
import { useTransition } from "react";
import { deleteItem } from "@/app/actions";
function DeleteButton({ id }: { id: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(() => deleteItem(id))}
>
{isPending ? "Deleting..." : "Delete"}
</button>
);
}Date, Map, Set, arrays tipados e objetos/arrays simplesFormData é suportado para submissões de formuláriouseActionState gerencia o estado do formulário e a validação, recebendo o estado anterior mais FormDatauseTransition apenas fornece um flag isPending e um wrapper startTransitionuseActionState quando precisar retornar erros ou status da action// A action recebe prevState como seu primeiro argumento
export async function createUser(
prevState: { error?: string },
formData: FormData
): Promise<{ error?: string }> {
"use server";
const email = formData.get("email") as string;
if (!email) return { error: "Email is required" };
// ...
return {};
}cookies() e headers() de next/headers dentro do corpo da actionawait)"use client";
import { useOptimistic, useTransition } from "react";
function TodoList({ todos }: { todos: Todo[] }) {
const [optimistic, addOptimistic] = useOptimistic(
todos,
(state, newText: string) => [
...state,
{ id: crypto.randomUUID(), text: newText, done: false },
]
);
// use addOptimistic dentro de startTransition antes de aguardar a action
}<form> com encType="multipart/form-data" ou construa FormData manualmenteJSON.stringify para dados de arquivoformData.get("file") que retorna um objeto FileuseTransition com actionsRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥