Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// 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.
// 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:
"use server" que exporta múltiples acciones del servidoruseActionState para gestión de estado de formulariosstartTransition"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.<form action={}> donde React automáticamente recopila FormData, o (2) llamadas directamente como await deleteItem(id) dentro de un startTransition.revalidatePath o revalidateTag provoca el re-renderizado de Componentes del Servidor afectados después de que la acción se complete.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>
);
}useActionState siguen la firma (prevState: T, formData: FormData) => Promise<T>.(id: string, formData: FormData) => Promise<void>.ActionResult (retornando { success, error? }) para manejo de errores seguro en tipos en lugar de lanzar excepciones."use server" requiere que la función sea async. Solución: Siempre declara acciones del servidor con async function.useOptimistic para actualizar la interfaz inmediatamente mientras las acciones se procesan en orden.cookies() o headers() de next/headers dentro del cuerpo de la acción.| 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é |
"use server" marca Funciones del Servidor (Acciones del Servidor), no Componentes del Servidor."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)."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.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>"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
}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>revalidatePath("/path") o revalidateTag("tag") dentro de la acción del servidoruseOptimistic para actualizar la interfaz inmediatamente mientras las acciones se procesan en orden"use server" son serializables a través del límite servidor-cliente"use server" dentro del cuerpo de la función; se cierran sobre variables del lado del servidor en el momento del renderizado"use server" en la parte superior del archivo y exportan múltiples accionestype 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 };
}(id: string, formData: FormData) => Promise<void>.bind(null, id), la firma de función resultante es (formData: FormData) => Promise<void>cookies() o headers() de next/headers dentro del cuerpo de la acción<form>Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥