Tipificación de Componentes de Servidor
Receta
Tipifica React Server Components (RSC), componentes async y Server Actions en Next.js. Entiende cómo TypeScript funciona con el límite servidor/cliente.
Ejemplo Funcional
// app/users/page.tsx - Componente de Servidor Async
type User = {
id: string;
name: string;
email: string;
};
async function getUsers(): Promise<User[]> {
const res = await fetch("https://api.example.com/users", {
cache: "force-cache",
});
if (!res.ok) throw new Error("Error al obtener usuarios");
return res.json() as Promise<User[]>;
}
export default async function UsersPage() {
const users = await getUsers();
return (
<main>
<h1>Usuarios</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}// Server Action con params tipificados
"use server";
type CreateUserInput = {
name: string;
email: string;
};
type ActionResult = {
success: boolean;
message: string;
};
export async function createUser(input: CreateUserInput): Promise<ActionResult> {
// Valida y guarda en la base de datos
if (!input.name || !input.email) {
return { success: false, message: "Nombre y email son requeridos" };
}
// ... operación de base de datos
return { success: true, message: "Usuario creado" };
}// Componente de cliente consumiendo el Server Action
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(
async (_prevState: ActionResult | null, formData: FormData) => {
const result = await createUser({
name: formData.get("name") as string,
email: formData.get("email") as string,
});
return result;
},
null
);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Creando..." : "Crear Usuario"}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}Análisis Profundo
Cómo Funciona
- Los Server Components pueden ser funciones
async. TypeScript permiteasync function Page()que devuelvePromise<JSX.Element>. Esto es único para RSC -- los componentes de cliente no pueden ser async. - Los Server Actions son funciones async marcadas con
"use server". Se ejecutan en el servidor pero pueden ser llamados desde componentes de cliente. TypeScript refuerza los tipos de parámetros y retorno a través del límite. - Las directivas
"use client"y"use server"crean límites de serialización. Solo los tipos serializables (strings, números, objetos simples, arrays, Date, FormData, etc.) pueden cruzar estos límites. useActionState(React 19) reemplaza el antiguouseFormState. Devuelve[state, formAction, isPending]con inferencia completa de tipos desde la función de acción.
Variaciones
Params de página tipificados (Next.js 15):
// app/users/[id]/page.tsx
type PageProps = {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function UserPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { tab } = await searchParams;
const user = await getUser(id);
return <div>{user.name}</div>;
}Server Action con FormData:
"use server";
export async function submitForm(formData: FormData): Promise<ActionResult> {
const name = formData.get("name");
const email = formData.get("email");
if (typeof name !== "string" || typeof email !== "string") {
return { success: false, message: "Datos de formulario inválidos" };
}
// ... procesa
return { success: true, message: "Enviado" };
}Componente de layout tipificado:
// app/layout.tsx
type RootLayoutProps = {
children: React.ReactNode;
};
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}Notas de TypeScript
- En Next.js 15,
paramsysearchParamsson tiposPromiseque deben ser awaited. Esto es un cambio importante respecto a Next.js 14. - Los Server Actions solo pueden aceptar y devolver valores serializables. Las funciones, instancias de clase y Symbols no pueden cruzar el límite.
- El tipo
React.FCno soporta componentes async. Usa declaraciones de función simples para Server Components.
Gotchas
- Importar un Server Action en un componente de cliente funciona, pero pasar una función de cliente a un Server Component no. Las funciones no son serializables.
formData.get()devuelveFormDataEntryValue | null(que esstring | File | null). Siempre valida y estrecha el tipo.- Olvidar
"use server"en un archivo de acción significa que la función se ejecuta en el cliente, lo que fallará si usa APIs solo del servidor como acceso a base de datos. - Los Server Components no pueden usar hooks (
useState,useEffect, etc.). TypeScript no atrapará esto -- es un error en tiempo de ejecución.
Alternativas
| Enfoque | Pros | Contras |
|---|---|---|
| Server Components Async | Obtención de datos directa, cero JS de cliente | No pueden usar hooks o APIs del navegador |
| Server Actions | Mutaciones con tipo seguro, mejora progresiva | Solo parámetros y retornos serializables |
| Manejadores de Rutas de API | Control HTTP completo, cualquier cliente puede llamar | Fetch manual + tipificación en cliente |
| tRPC | Seguridad de tipos de extremo a extremo | Dependencia adicional y setup |
getServerSideProps del lado del servidor (Pages Router) | Patrón familiar | Solo Pages Router, siendo descontinuado |
FAQs
¿Puede un Server Component ser una función async?
- Sí. Los Server Components pueden ser
asyncy devolverPromise<JSX.Element>. - Esto es único para RSC -- los componentes de cliente no pueden ser async.
¿Qué es la directiva "use server" y dónde va?
- Marca un archivo (o función individual) como un Server Action.
- Colócala en la parte superior del archivo o al inicio del cuerpo de la función.
- Los Server Actions se ejecutan en el servidor pero pueden ser llamados desde componentes de cliente.
¿Qué tipos pueden cruzar el límite servidor/cliente?
- Solo tipos serializables: strings, números, booleanos, objetos simples, arrays, Date, FormData.
- Las funciones, instancias de clase, Symbols y Maps no pueden cruzar el límite.
¿Cómo tipificar params de página en Next.js 15?
type PageProps = {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function Page({ params, searchParams }: PageProps) {
const { id } = await params;
}¿Qué es useActionState y cómo reemplaza a useFormState?
useActionState(React 19) devuelve[state, formAction, isPending].- Reemplaza el antiguo
useFormStatecon inferencia completa de tipos y una bandera de pendencia integrada.
Gotcha: ¿Qué sucede si olvido la directiva "use server" en un archivo de acción?
- La función se ejecutará en el cliente en lugar del servidor.
- Si usa APIs solo del servidor (base de datos, secretos), fallará en tiempo de ejecución.
¿Puedo usar React.FC para Server Components?
- No.
React.FCno soporta componentes async. - Usa declaraciones de función simples:
async function Page() { ... }.
Gotcha: ¿Pueden los Server Components usar hooks como useState o useEffect?
- No. Los Server Components no pueden usar ningún hook.
- TypeScript no atrapará esto -- produce un error en tiempo de ejecución, no un error en tiempo de compilación.
¿Cómo debo tipificar un componente de layout?
type RootLayoutProps = {
children: React.ReactNode;
};
export default function RootLayout({ children }: RootLayoutProps) {
return <html><body>{children}</body></html>;
}¿Qué devuelve formData.get() y cómo lo estrecho?
- Devuelve
FormDataEntryValue | null, que esstring | File | null. - Siempre verifica
typeof value === "string"antes de usarlo como string.
¿Puedo pasar una función de cliente a un Server Component?
- No. Las funciones no son serializables y no pueden cruzar el límite cliente-a-servidor.
- Importar un Server Action en un componente de cliente funciona, pero lo inverso no.
¿Cómo tipificar un Server Action que acepta FormData?
"use server";
export async function submitForm(formData: FormData): Promise<ActionResult> {
const name = formData.get("name");
if (typeof name !== "string") {
return { success: false, message: "Inválido" };
}
// procesa...
return { success: true, message: "Hecho" };
}