Server Actions y Data Fetching
¿Qué son Server Actions?
Los Server Actions son funciones que se ejecutan en el servidor, invocadas directamente desde componentes del cliente. Se definen con la directiva "use server":
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Ejecuta en el servidor — acceso directo a DB, APIs, etc.
await db.posts.create({ title, content });
}
Formularios con Server Actions
Formulario básico
// app/new-post/page.tsx
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<label>
Título:
<input name="title" required />
</label>
<label>
Contenido:
<textarea name="content" required />
</label>
<button type="submit">Crear post</button>
</form>
);
}
El formulario envía los datos al servidor sin JavaScript del lado del cliente. Funciona incluso con JS deshabilitado gracias a progressive enhancement.
Formulario con Client Component
Para feedback visual durante el envío:
// app/components/post-form.tsx
"use client";
import { useActionState } from "react";
import { createPost } from "../actions";
export function PostForm() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action}>
<input name="title" required disabled={isPending} />
<textarea name="content" required disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Creando..." : "Crear post"}
</button>
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}
Server Action con retorno
// app/actions.ts
"use server";
interface ActionResult {
error?: string;
success?: boolean;
}
export async function createPost(
_prevState: ActionResult | null,
formData: FormData
): Promise<ActionResult> {
const title = formData.get("title") as string;
if (title.length < 3) {
return { error: "El título debe tener al menos 3 caracteres" };
}
await db.posts.create({ title });
return { success: true };
}
Server Actions inline
Puedes definir acciones directamente en Server Components:
// app/settings/page.tsx
export default function SettingsPage() {
async function updateTheme(formData: FormData) {
"use server";
const theme = formData.get("theme") as string;
await db.settings.update({ theme });
}
return (
<form action={updateTheme}>
<select name="theme">
<option value="light">Claro</option>
<option value="dark">Oscuro</option>
</select>
<button type="submit">Guardar</button>
</form>
);
}
Redirect después de una acción
"use server";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const post = await db.posts.create({
title: formData.get("title") as string,
});
redirect(`/posts/${post.id}`);
}
Revalidación de datos
revalidatePath
Invalida el caché de una ruta específica:
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
await db.posts.create({ title: formData.get("title") as string });
revalidatePath("/posts"); // Refresca la lista de posts
}
revalidateTag
Invalida por tags semánticos:
// Fetch con tag
const posts = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
// Invalidar el tag
"use server";
import { revalidateTag } from "next/cache";
export async function createPost(formData: FormData) {
await db.posts.create({ title: formData.get("title") as string });
revalidateTag("posts");
}
next/form
Vinext soporta next/form para interceptación de formularios:
import Form from "next/form";
export default function SearchPage() {
return (
<Form action="/search">
<input name="q" placeholder="Buscar..." />
<button type="submit">Buscar</button>
</Form>
);
}
next/form intercepta el submit del formulario GET y realiza navegación client-side, actualizando la URL sin full page reload.
”use cache”
Vinext soporta la directiva experimental "use cache" para caché declarativo:
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export async function getPosts() {
cacheLife("hours");
cacheTag("posts");
const res = await fetch("https://api.example.com/posts");
return res.json();
}
Siguiente paso
En el siguiente capítulo exploraremos navegación optimizada con next/link, imágenes con next/image y la Metadata API.