Server Actions y Data Fetching

Por: Artiko
vinextserver-actionsformscacherevalidation

¿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.