Acciones del Servidor - Llama funciones del lado del servidor directamente desde componentes cliente
Receta
// app/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const body = formData.get("body") as string;
await db.insert("posts", { title, body, createdAt: new Date() });
revalidatePath("/posts");
}// app/posts/NewPostForm.tsx
"use client";
import { createPost } from "../actions";
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Write your post..." required />
<button type="submit">Publish</button>
</form>
);
}Cuándo usarlo: Usa Acciones del Servidor para cualquier mutación de datos (crear, actualizar, eliminar) que necesite ejecutarse en el servidor -- escrituras en la base de datos, cargas de archivos, envío de emails, llamadas a APIs de terceros con secretos.
Ejemplo Completo
// Una aplicación de tareas completa con acciones del servidor, validación y manejo de errores
// app/actions.ts
"use server";
import { z } from "zod";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
const TodoSchema = z.object({
text: z.string().min(1, "Todo text is required").max(200, "Too long"),
});
export type ActionResult = {
success: boolean;
error?: string;
};
export async function addTodo(_prev: ActionResult, formData: FormData): Promise<ActionResult> {
const parsed = TodoSchema.safeParse({ text: formData.get("text") });
if (!parsed.success) {
return { success: false, error: parsed.error.errors[0].message };
}
try {
await db.insert("todos", {
text: parsed.data.text,
completed: false,
createdAt: new Date(),
});
revalidatePath("/todos");
return { success: true };
} catch {
return { success: false, error: "Failed to save todo" };
}
}
export async function toggleTodo(id: string) {
const todo = await db.findById("todos", id);
if (!todo) throw new Error("Not found");
await db.update("todos", id, { completed: !todo.completed });
revalidatePath("/todos");
}
export async function deleteTodo(id: string) {
await db.delete("todos", id);
revalidatePath("/todos");
}// app/todos/TodoApp.tsx
"use client";
import { useActionState, useOptimistic, useTransition } from "react";
import { addTodo, toggleTodo, deleteTodo, type ActionResult } from "../actions";
type Todo = { id: string; text: string; completed: boolean };
export default function TodoApp({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newText: string) => [
...state,
{ id: "optimistic", text: newText, completed: false },
]
);
const [state, formAction, isPending] = useActionState(
async (prev: ActionResult, formData: FormData) => {
const text = formData.get("text") as string;
addOptimisticTodo(text);
return addTodo(prev, formData);
},
{ success: true }
);
return (
<div>
<h1>Tareas</h1>
<form action={formAction}>
<input name="text" placeholder="What needs doing?" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Adding..." : "Add"}
</button>
{state.error && <p className="text-red-500">{state.error}</p>}
</form>
<ul>
{optimisticTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</div>
);
}
function TodoItem({ todo }: { todo: Todo }) {
const [isPending, startTransition] = useTransition();
return (
<li className={isPending ? "opacity-50" : ""}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => startTransition(() => toggleTodo(todo.id))}
/>
<span className={todo.completed ? "line-through" : ""}>{todo.text}</span>
</label>
<button onClick={() => startTransition(() => deleteTodo(todo.id))}>
Delete
</button>
</li>
);
}Lo que esto demuestra:
- Un archivo
"use server"que exporta múltiples acciones del servidor - Validación con Zod dentro de una acción del servidor
- Retorno de resultados estructurados (éxito/error) de acciones
- Vinculación de una acción del servidor a
useActionStatepara gestión de estado de formularios - Llamada de acciones del servidor desde manejadores de eventos a través de
startTransition - Actualizaciones optimistas superpuestas sobre acciones del servidor
Análisis Profundo
Cómo Funciona
- Una función (o archivo) marcada con
"use server"le dice al empaquetador que cree un punto final de red (RPC) para esa función. El cliente recibe un ID de referencia, no el cuerpo de la función. - Cuando un cliente llama una acción del servidor, React serializa los argumentos, envía un POST HTTP al servidor, ejecuta la función y devuelve el resultado serializado.
- Las acciones del servidor se pueden usar de dos formas: (1) pasadas a
<form action={}>donde React automáticamente recopilaFormData, o (2) llamadas directamente comoawait deleteItem(id)dentro de unstartTransition. - Los argumentos y valores de retorno deben ser serializables -- las mismas reglas que aplican para props de Componentes del Servidor aplican.
- Las acciones del servidor se ejecutan secuencialmente por defecto por solicitud. Los envíos de formularios múltiples se encolan, no se ejecutan en paralelo.
- En marcos como Next.js,
revalidatePathorevalidateTagprovoca el re-renderizado de Componentes del Servidor afectados después de que la acción se complete.
Variaciones
Acciones del servidor en línea en Componentes del Servidor:
// La acción se define en línea y se cierra sobre datos del lado del servidor
export default async function LikePage() {
let likes = await db.getLikes();
async function addLike() {
"use server";
await db.incrementLikes();
}
return (
<div>
<p>Likes: {likes}</p>
<form action={addLike}>
<button type="submit">Like</button>
</form>
</div>
);
}Llamar acciones del servidor fuera de formularios:
"use client";
import { useTransition } from "react";
import { deleteItem } from "./actions";
function DeleteButton({ id }: { id: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(async () => {
await deleteItem(id);
})}
>
{isPending ? "Deleting..." : "Delete"}
</button>
);
}Vinculación de argumentos adicionales con .bind:
// Aplicación parcial del id para que el formulario solo envíe FormData
import { updateItem } from "./actions";
function EditForm({ id }: { id: string }) {
const updateWithId = updateItem.bind(null, id);
return (
<form action={updateWithId}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}Notas de TypeScript
- Las acciones del servidor usadas con
useActionStatesiguen la firma(prevState: T, formData: FormData) => Promise<T>. - Cuando vinculamos argumentos adicionales, los argumentos vinculados van primero:
(id: string, formData: FormData) => Promise<void>. - Usa el patrón
ActionResult(retornando{ success, error? }) para manejo de errores seguro en tipos en lugar de lanzar excepciones.
Trampas
- Las acciones del servidor deben ser async -- La directiva
"use server"requiere que la función seaasync. Solución: Siempre declara acciones del servidor conasync function. - Los argumentos deben ser serializables -- No puedes pasar nodos del DOM, instancias de clases, u objetos no serializables. Solución: Extrae los datos serializables que necesitas (strings, números, IDs) antes de llamar la acción.
- Lanzar errores en acciones -- Si una acción del servidor lanza, el error cruza el límite de red y puede bloquear el cliente. Solución: Captura errores dentro de la acción y devuelve un objeto de resultado. Usa límites de error como red de seguridad.
- Las acciones se encolan secuencialmente -- Los envíos de formularios rápidos se encolan, no se paralelizan. La interfaz puede sentirse lenta. Solución: Usa
useOptimisticpara actualizar la interfaz inmediatamente mientras las acciones se procesan en orden. - Captura de cierre en acciones en línea -- Las acciones del servidor en línea se cierran sobre variables del lado del servidor en el momento del renderizado. Esos valores se serializan y se envían al cliente como argumentos ocultos cifrados. Solución: Ten en cuenta que los cierres aumentan el tamaño de la carga. Prefiere archivos de acciones para lógica compleja.
- Sin acceso a encabezados de solicitud por defecto -- Las acciones del servidor no reciben automáticamente cookies o encabezados como argumentos. Solución: Usa ayudantes proporcionados por el marco como
cookies()oheaders()denext/headersdentro del cuerpo de la acción.
Alternativas
| Enfoque | Cuándo elegir |
|---|---|
| Acciones del Servidor | Mutaciones de datos estándar en React 19 con soporte de marco |
| Rutas API | Cuando necesitas puntos finales REST consumidos por clientes no React |
| tRPC | Seguridad de tipos de extremo a extremo sin convenciones específicas del marco |
| Mutaciones de GraphQL | Gráficos de datos complejos con múltiples clientes |
| fetch del lado del cliente + API | Cuando necesitas control total sobre método HTTP, encabezados, almacenamiento en caché |
Preguntas Frecuentes
¿Marca "use server" un archivo como un Componente del Servidor?
- No -- esto es un malentendido común.
"use server"marca Funciones del Servidor (Acciones del Servidor), no Componentes del Servidor. - Los Componentes del Servidor son el defecto en React 19 / Next.js App Router -- no se necesita directiva.
- Solo agregas
"use client"para optar por no usar Componentes del Servidor. "use server"es exclusivamente para funciones que se ejecutan en el servidor pero son llamadas desde código cliente (formularios, manejadores de eventos).- Poner
"use server"en la parte superior de un archivo de componente convertirá cada exportación en una Acción del Servidor, no un Componente del Servidor -- y generará error porque los componentes no son acciones válidas.
¿Qué hace la directiva "use server"?
- Le dice al empaquetador que cree un punto final de red (RPC) para la función
- El cliente recibe un ID de referencia, no el cuerpo de la función
- Cuando se llama desde el cliente, React serializa argumentos, envía un POST HTTP, ejecuta en el servidor y devuelve el resultado serializado
¿Pueden usarse acciones del servidor fuera de formularios?
Sí. Las acciones del servidor se pueden llamar directamente desde manejadores de eventos envueltos en startTransition:
const [isPending, startTransition] = useTransition();
<button onClick={() => startTransition(async () => {
await deleteItem(id);
})}>
Delete
</button>¿Cómo validas entrada dentro de una acción del servidor?
"use server";
import { z } from "zod";
const Schema = z.object({
text: z.string().min(1).max(200),
});
export async function addItem(_prev: Result, formData: FormData) {
const parsed = Schema.safeParse({ text: formData.get("text") });
if (!parsed.success) {
return { success: false, error: parsed.error.errors[0].message };
}
// proceed with valid data
}¿Cómo vinculadas argumentos adicionales a una acción del servidor?
Usa .bind() para aplicación parcial de argumentos para que el formulario solo envíe FormData:
const updateWithId = updateItem.bind(null, id);
<form action={updateWithId}>
<input name="name" />
<button type="submit">Save</button>
</form>¿Cómo desencadenas revalidación después de que una acción del servidor se completa?
- En Next.js, usa
revalidatePath("/path")orevalidateTag("tag")dentro de la acción del servidor - Esto desencadena re-renderizado de Componentes del Servidor afectados con datos frescos
- La revalidación se ejecuta después de que la función de acción devuelve
¿Se procesan múltiples envíos de formularios en paralelo?
- No. Las acciones del servidor se ejecutan secuencialmente por defecto por solicitud
- Los envíos de formularios rápidos se encolan, no se paralelizan
- Usa
useOptimisticpara actualizar la interfaz inmediatamente mientras las acciones se procesan en orden
Trampa: ¿Qué sucede si una acción del servidor lanza un error?
- El error cruza el límite de red y puede bloquear el cliente
- Siempre captura errores dentro de la acción y devuelve un objeto de resultado en lugar de lanzar
- Usa límites de error como red de seguridad para fallos inesperados
Trampa: ¿Puedes pasar una función regular (no una acción del servidor) desde un Componente del Servidor a un Componente Cliente?
- No. Solo funciones marcadas con
"use server"son serializables a través del límite servidor-cliente - Los cierres regulares, instancias de clases y nodos del DOM no se pueden pasar como props a Componentes Cliente
- Extrae los datos serializables que necesitas (strings, números, IDs) antes de pasar
¿Cuál es la diferencia entre acciones del servidor en línea y archivos de acciones?
- Las acciones en línea se definen dentro de un Componente del Servidor con
"use server"dentro del cuerpo de la función; se cierran sobre variables del lado del servidor en el momento del renderizado - Los archivos de acciones tienen
"use server"en la parte superior del archivo y exportan múltiples acciones - Los cierres en línea aumentan el tamaño de la carga porque los valores capturados se serializan; prefiere archivos de acciones para lógica compleja
¿Cómo escribes una acción del servidor usada con useActionState en TypeScript?
type ActionResult = { success: boolean; error?: string };
// Firma: (prevState: T, formData: FormData) => Promise<T>
export async function myAction(
_prev: ActionResult,
formData: FormData
): Promise<ActionResult> {
return { success: true };
}¿Cómo escribes una acción del servidor con argumentos vinculados en TypeScript?
- Los argumentos vinculados van primero en la firma de la función:
(id: string, formData: FormData) => Promise<void> - Después de
.bind(null, id), la firma de función resultante es(formData: FormData) => Promise<void>
¿Cómo accedes a cookies o encabezados dentro de una acción del servidor?
- Las acciones del servidor no reciben automáticamente cookies o encabezados como argumentos
- Usa ayudantes proporcionados por el marco como
cookies()oheaders()denext/headersdentro del cuerpo de la acción
Relacionado
- Componentes del Servidor -- El modelo de renderizado que las acciones del servidor complementan
- Acciones de Formulario -- Conexión de acciones del servidor a elementos
<form> - useOptimistic -- Mostrar retroalimentación instantánea mientras se ejecutan acciones
- Hook use() -- Consumir datos devueltos por acciones del servidor