En el desarrollo web clásico, cualquier mutación de datos (añadir un comentario, guardar un perfil, realizar un pedido) requería la creación de un endpoint REST o GraphQL separado (/api/comments), la serialización manual a JSON.stringify(), una llamada a fetch() en el cliente, el manejo de errores de red y una compleja invalidación de la caché local.
En Next.js 16 y React 19, esta capa engorrosa se reemplaza por Server Actions (Acciones de Servidor): funciones asíncronas del servidor, marcadas con la directiva 'use server'. Se pueden invocar directamente desde formularios HTML normales (<form action={myServerAction}>) o desde componentes de cliente como funciones asíncronas estándar de JavaScript.
Bajo el capó, Next.js automáticamente:
Action-ID.FormData estándar.revalidatePath() o revalidateTag().Veamos cómo una Server Action une el formulario, el backend y el restablecimiento de la caché:
+-------------------------------------------------------------------------+
| ARQUITECTURA DE SERVER ACTIONS EN NEXT.JS 16 |
+-------------------------------------------------------------------------+
| NAVEGADOR (Cliente): |
| <form action={createPost}> |
| <input name="title" /> |
| <button type="submit">Publicar</button> |
| </form> |
+-------------------------------------------------------------------------+
| HTTP POST (Llamada RPC automática de Next.js) |
| | |
| v |
+-------------------------------------------------------------------------+
| SERVIDOR (Node.js runtime): |
| async function createPost(formData: FormData) { |
| 'use server'; |
| const title = formData.get('title'); |
| await db.insert(posts).values({ title }); // 1. Escritura en la DB |
| revalidatePath('/posts'); // 2. Invalidación de caché|
| } |
+-------------------------------------------------------------------------+
| RESPUESTA AL CLIENTE: |
| ¡Next.js devuelve instantáneamente el HTML fresco con la nueva |
| publicación en la lista! |
| (¡Sin refetchQueries() manuales ni window.location.reload()!) |
+-------------------------------------------------------------------------+El usuario hace clic en el botón de enviar. Los datos del formulario se empaquetan en un objeto FormData.
Next.js envía una solicitud POST con un identificador único de la acción del servidor.
La función con la directiva 'use server' valida los datos de entrada (Zod) y los guarda en la base de datos.
Next.js restablece las páginas cacheadas y devuelve el HTML actualizado al cliente.
¿Qué papel desempeña la directiva `'use server'` en Next.js 16?
app/api/....httpOnly seguras y redirección a la página principal.revalidateTag('cart')).Dentro de un componente de servidor, una Server Action puede declararse directamente dentro del cuerpo del archivo:
import { revalidatePath } from 'next/cache';
export default function FeedbackPage() {
// ¡Declaramos la Server Action justo aquí!
async function submitFeedback(formData: FormData) {
'use server'; // ¡OBLIGATORIO: declara la acción del servidor!
const email = formData.get('email') as string;
const message = formData.get('message') as string;
// Simulación de escritura en la base de datos
console.log(`[Backend] Comentario recibido de ${email}: ${message}`);
// Limpiamos la caché de la página de comentarios para mostrar los datos actualizados
revalidatePath('/feedback');
}
return (
<div className="max-w-md mx-auto py-12">
<h1 className="text-2xl font-bold mb-6">Dejar un comentario</h1>
{/* ¡El formulario está directamente vinculado a la acción del servidor! */}
<form action={submitFeedback} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-text-muted mb-1">
Tu Email
</label>
<input
name="email"
type="email"
required
className="w-full px-4 py-2 rounded-lg border border-border bg-bg-surface text-text-primary"
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-muted mb-1">
Mensaje
</label>
<textarea
name="message"
rows={4}
required
className="w-full px-4 py-2 rounded-lg border border-border bg-bg-surface text-text-primary"
/>
</div>
<button
type="submit"
className="w-full py-2.5 px-4 rounded-xl bg-accent-cyan text-bg-base font-semibold hover:opacity-90 transition-opacity"
>
Enviar Comentario
</button>
</form>
</div>
);
}Si el formulario necesita ser interactivo en el cliente ('use client'), las Server Actions se extraen a un archivo separado (por ejemplo, app/actions.ts):
'use server'; // ¡Todo el archivo exporta Server Actions!
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function deletePost(postId: string) {
// 1. Verificación de autorización de la sesión actual
// 2. Eliminación de la base de datos
console.log(`Publicación eliminada: ${postId}`);
// 3. Restablecimiento de la caché del servidor de la lista de publicaciones
revalidatePath('/posts');
// 4. Redirección opcional
redirect('/posts');
}useActionState (React 19)#En React 19, el hook estándar useActionState se utiliza para gestionar el estado de envío del formulario:
'use client';
import React, { useActionState } from 'react';
// Acción del servidor con retorno de estado tipado
async function subscribeAction(prevState: { message: string; success: boolean }, formData: FormData) {
'use server';
const email = formData.get('email');
if (!email || typeof email !== 'string') {
return { success: false, message: 'Email inválido' };
}
return { success: true, message: '¡Te has suscrito correctamente al boletín!' };
}
export function SubscribeForm() {
// useActionState devuelve [state, formAction, isPending]
const [state, formAction, isPending] = useActionState(subscribeAction, {
success: false,
message: '',
});
return (
<form action={formAction} className="flex flex-col gap-2">
<div className="flex gap-2">
<input
name="email"
type="email"
placeholder="Tu email..."
required
disabled={isPending}
className="px-4 py-2 rounded-lg border border-border bg-bg-surface"
/>
<button
type="submit"
disabled={isPending}
className="btn-primary"
>
{isPending ? 'Enviando...' : 'Suscribirse'}
</button>
</div>
{state.message && (
<p className={`text-xs ${state.success ? 'text-emerald-400' : 'text-rose-400'}`}>
{state.message}
</p>
)}
</form>
);
}¿Qué tarea crítica resuelve la llamada a la función `revalidatePath('/products')` dentro de una Server Action después de añadir un nuevo producto?
¡Nunca confíes en los datos de formData sin validación! El cliente puede enviar cualquier cadena.
Solución: Siempre valida los parámetros de entrada a través de Zod o Valibot dentro de la Server Action:
const schema = z.object({ email: z.string().email() });
const parsed = schema.safeParse({ email: formData.get('email') });
if (!parsed.success) throw new Error('Email inválido');La directiva 'use server' dentro de un archivo con 'use client' solo es admisible como una llamada de función separada, pero está prohibido declarar Server Actions en línea dentro de un Client Component.
Solución: Crea un archivo app/actions.ts separado con 'use server' en la primera línea e importa las funciones desde allí.