Acciones del Servidor
Mutar datos desde el servidor usando funciones "use server" -- sin necesidad de rutas API.
Receta
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.
Ejemplo Funcional
// 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:
- Definir Server Actions en un archivo
"use server"separado - Vincular una Server Action a una
<form action>para mejora progresiva - Llamar Server Actions desde Componentes de Cliente a través de
useTransitionpara UI pendiente - Usar
revalidatePathpara actualizar datos en caché después de mutaciones
Análisis Profundo
Cómo Funciona
- Una Server Action es cualquier función
asyncmarcada 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). - Cuando se pasa a una
<form action>, React envía los datos del formulario al servidor y llama la función. Esto funciona incluso sin JavaScript habilitado (mejora progresiva). - Cuando se llama desde un manejador de eventos de un Componente de Cliente, la acción se invoca como un RPC -- React serializa los argumentos, los envía al servidor y devuelve el resultado.
- Server Actions se ejecutan en un contexto seguro del servidor. Pueden acceder a bases de datos, variables de entorno y sistemas de archivos directamente.
- Después de una mutación, llama
revalidatePath()orevalidateTag()para invalidar datos en caché e desencadenar una renderización fresca. - Server Actions son solicitudes POST bajo el capó. Están protegidas automáticamente contra CSRF a través de comprobaciones del mismo origen.
Variaciones
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>
);
}Notas de TypeScript
// 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";
// ...
}Advertencias
-
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,Seto arrays tipados. -
revalidatePathno funciona dentro de try/catch con redirect -- Llamarredirect()lanza un error especial de Next.js. Si lo envuelves en try/catch, el redirect se traga. Solución: Llamaredirect()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 usarJSON.stringifypara archivos. Solución: Usa un<form>conencType="multipart/form-data"o construyeFormDatamanualmente en el cliente. -
Sin acceso a encabezados de solicitud por defecto -- Server Actions no reciben automáticamente cookies o encabezados. Solución: Usa
cookies()yheaders()desdenext/headersdentro 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.
Alternativas
| 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 |
Preguntas Frecuentes
¿Qué hace realmente la directiva "use server"?
- Marca funciones async como Server Actions que se ejecutan exclusivamente en el servidor
- Puede colocarse en la parte superior de un archivo (marca todas las exportaciones) o en línea dentro del cuerpo de una función en un Server Component
- React serializa los argumentos, los envía al servidor y llama la función como un RPC
¿Funcionan Server Actions sin JavaScript habilitado en el navegador?
- Sí, cuando se usa con
<form action>, Server Actions admiten mejora progresiva - El formulario se envía como una solicitud POST estándar cuando JS está deshabilitado
- Esto no se aplica cuando llamas acciones desde manejadores de eventos como
onClick
¿Por qué no funciona llamar redirect() dentro de un bloque try/catch?
redirect()lanza un error especial de Next.js internamente para desencadenar navegación- Envolverlo en try/catch traga este error e impide el redirect
- Siempre llama
redirect()fuera del bloque try/catch, después de toda la otra lógica
¿Cómo muestras un estado pendiente cuando se ejecuta una Server Action?
"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>
);
}¿Qué tipos de argumentos puedes pasar a una Server Action?
- Strings, números, booleanos,
Date,Map,Set, arrays tipados y objetos/arrays simples FormDatase admite para envíos de formularios- No puedes pasar nodos DOM, instancias de clases o funciones
¿Cuál es la diferencia entre useActionState y useTransition para Server Actions?
useActionStateadministra estado del formulario y validación, recibiendo estado anterior másFormDatauseTransitionsolo proporciona una banderaisPendingy un contenedorstartTransition- Usa
useActionStatecuando necesites devolver errores o estado desde la acción
¿Cómo tipeas una Server Action que se usa con useActionState?
// 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 {};
}¿Cómo se protegen Server Actions contra ataques CSRF?
- Server Actions son solicitudes POST bajo el capó
- Next.js aplica automáticamente comprobaciones del mismo origen
- No se necesita configuración adicional de token CSRF
¿Puedes acceder a cookies o encabezados dentro de una Server Action?
- Server Actions no reciben automáticamente cookies o encabezados
- Importa y llama
cookies()yheaders()desdenext/headersdentro del cuerpo de la acción - Ambos son async en Next.js 15+ y deben ser awaited
¿Qué sucede si una acción en línea "use server" cierra sobre una variable sensible?
- La variable que cierra se serializa y se envía al cliente como parte de la referencia de acción
- Esto puede exponer inadvertidamente datos sensibles
- Siempre valida todas las entradas en el servidor y evita cerrar sobre secretos
¿Cómo implementas actualizaciones optimistas con Server Actions?
"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
}¿Cómo manejas cargas de archivos con Server Actions?
- Usa un
<form>conencType="multipart/form-data"o construyeFormDatamanualmente - No puedes usar
JSON.stringifypara datos de archivos - Accede a archivos a través de
formData.get("file")que devuelve un objetoFile
Relacionado
- Fetching -- Lectura de datos en Server Components
- Revalidation -- Actualización de datos en caché después de mutaciones
- Cookies & Headers -- Acceso a contexto de solicitud en acciones
- Client Components -- Usar
useTransitioncon acciones