Acciones del Servidor
Mutar datos desde el servidor usando funciones "use server" -- sin necesidad de rutas API.
Busca en todas las páginas de la documentación
Mutar datos desde el servidor usando funciones "use server" -- sin necesidad de rutas API.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida -- lista para copiar y pegar.
// 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">Crear</button>
</form>
);
}Cuándo usarlo: Necesitas crear, actualizar o eliminar datos desde un formulario o clic de botón sin escribir una ruta 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: "Se requiere texto" };
}
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: "No encontrado" };
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">Tareas</h1>
<form action={addTodo} className="flex gap-2 mb-6">
<input
name="text"
placeholder="¿Qué necesita hacerse?"
className="flex-1 border rounded px-3 py-2"
required
/>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Añadir
</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"
>
Eliminar
</button>
</li>
);
}Lo que esto demuestra:
"use server" separado<form action> para mejora progresivauseTransition para UI pendienterevalidatePath para actualizar datos en caché después de mutacionesasync marcada con "use server" (ya sea en la parte superior de un archivo o en línea dentro del cuerpo de una función en un Server Component).<form action>, React envía los datos del formulario al servidor y llama la función. Esto funciona incluso sin JavaScript habilitado (mejora progresiva).revalidatePath() o revalidateTag() para invalidar datos en caché e desencadenar una renderización fresca.Acción del Servidor en Línea (definida dentro de un 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">Suscribirse</button>
</form>
);
}Usar useActionState para estado del formulario y validación:
"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 ? "Creando..." : "Registrarse"}
</button>
</form>
);
}Actualizaciones optimistas con 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">Añadir</button>
<ul>
{optimisticTodos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
</form>
);
}// Firma de Server Action cuando se usa con <form action>
export async function myAction(formData: FormData): Promise<void> {
"use server";
// ...
}
// Server Action con useActionState -- recibe estado anterior + formData
export async function myAction(
prevState: { error?: string },
formData: FormData
): Promise<{ error?: string }> {
"use server";
// ...
}
// Server Action llamada directamente (no desde un formulario)
export async function deleteItem(id: string): Promise<{ success: boolean }> {
"use server";
// ...
}Los argumentos deben ser serializables -- No puedes pasar nodos DOM, instancias de clases o funciones a una Server Action. Solución: Pasa solo strings, números, booleanos, FormData, objetos simples, arrays, Date, Map, Set o arrays tipados.
revalidatePath no funciona dentro de try/catch con redirect -- Llamar redirect() lanza un error especial de Next.js. Si lo envuelves en try/catch, el redirect se traga. Solución: Llama redirect() fuera del bloque try/catch, después de toda la otra lógica.
Server Actions son solo POST -- No pueden usarse para solicitudes GET o lecturas idempotentes. Solución: Usa un fetch regular o Server Component para leer datos.
Las cargas de archivos necesitan FormData -- No puedes usar JSON.stringify para archivos. Solución: Usa un <form> con encType="multipart/form-data" o construye FormData manualmente en el cliente.
Sin acceso a encabezados de solicitud por defecto -- Server Actions no reciben automáticamente cookies o encabezados. Solución: Usa cookies() y headers() desde next/headers dentro del cuerpo de la acción.
Las variables de cierre se serializan -- Si defines una acción en línea "use server" que cierra sobre una variable, esa variable se serializa y se envía con la acción. Solución: Ten cuidado con datos sensibles en cierres; valida todas las entradas en el servidor.
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Route Handlers (rutas API) | Necesitas un endpoint REST para clientes externos o webhooks | Solo mutas datos desde tu propia UI |
SWR mutate + ruta API | Necesitas invalidación de caché instantánea en el cliente con SWR | Quieres mejora progresiva |
| tRPC | Quieres seguridad de tipos de extremo a extremo con una capa RPC dedicada | Quieres mantener cosas nativas del framework |
fetch desde Componente de Cliente | Necesitas control total sobre el manejo de solicitud/respuesta | Server Actions ya cubren tu caso de uso |
<form action>, Server Actions admiten mejora progresivaonClickredirect() lanza un error especial de Next.js internamente para desencadenar navegaciónredirect() fuera del bloque try/catch, después de toda la otra 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 ? "Eliminando..." : "Eliminar"}
</button>
);
}Date, Map, Set, arrays tipados y objetos/arrays simplesFormData se admite para envíos de formulariosuseActionState administra estado del formulario y validación, recibiendo estado anterior más FormDatauseTransition solo proporciona una bandera isPending y un contenedor startTransitionuseActionState cuando necesites devolver errores o estado desde la acción// La acción recibe prevState como su primer 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: "Se requiere correo electrónico" };
// ...
return {};
}cookies() y headers() desde next/headers dentro del cuerpo de la acción"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 },
]
);
// usa addOptimistic dentro de startTransition antes de awaitar la acción
}<form> con encType="multipart/form-data" o construye FormData manualmenteJSON.stringify para datos de archivosformData.get("file") que devuelve un objeto FileuseTransition con accionesRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥