SSR y React Server Components
Server Components por defecto
En el App Router de Vinext, todos los componentes son Server Components por defecto. Se ejecutan en el servidor y nunca envían JavaScript al cliente:
// app/users/page.tsx — Server Component (por defecto)
export default async function UsersPage() {
const users = await fetch("https://api.example.com/users").then(r => r.json());
return (
<ul>
{users.map((user: { id: number; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Este componente:
- Ejecuta
fetchen el servidor - Renderiza HTML y lo envía al cliente
- No incluye JavaScript en el bundle del cliente
Client Components
Cuando necesitas interactividad (estado, efectos, event handlers), usa la directiva "use client":
// app/components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Incrementar</button>
</div>
);
}
// app/page.tsx — Server Component que usa un Client Component
import { Counter } from "./components/counter";
export default function HomePage() {
return (
<main>
<h1>Renderizado en servidor: {new Date().toISOString()}</h1>
<Counter />
</main>
);
}
El <h1> se renderiza en el servidor. El <Counter> se hidrata en el cliente con su JavaScript.
Cuándo usar cada tipo
| Necesitas… | Server Component | Client Component |
|---|---|---|
| Fetch de datos | ✓ | |
| Acceso a backend directo | ✓ | |
| Tokens/secretos | ✓ | |
| Reducir bundle JS | ✓ | |
| Estado (useState) | ✓ | |
| Efectos (useEffect) | ✓ | |
| Event handlers (onClick) | ✓ | |
| Browser APIs | ✓ |
Streaming SSR
Vinext soporta streaming en ambos routers. El HTML se envía progresivamente al cliente:
- El servidor envía el shell HTML con los loading states
- A medida que los datos se resuelven, el contenido reemplaza los placeholders
- El usuario ve contenido útil antes de que toda la página esté lista
Cómo funciona con loading.tsx
app/
layout.tsx # Se envía inmediatamente
page.tsx # Contenido principal
dashboard/
loading.tsx # Se envía como placeholder
page.tsx # Se inyecta cuando los datos están listos
El flujo es:
- El servidor envía
layout.tsx+loading.tsxcomo HTML inicial - Mientras,
dashboard/page.tsxresuelve sus datos - El HTML del
page.tsxse inyecta vía streaming, reemplazando el loading
Streaming manual con Suspense
import { Suspense } from "react";
async function SlowData() {
const data = await fetch("https://api.example.com/slow");
const result = await data.json();
return <p>{result.message}</p>;
}
export default function Page() {
return (
<div>
<h1>Datos inmediatos</h1>
<Suspense fallback={<p>Cargando datos lentos...</p>}>
<SlowData />
</Suspense>
</div>
);
}
Arquitectura multi-ambiente
El aspecto más técnico de Vinext: RSC y SSR operan en grafos de módulos separados. Esto significa que:
- El entorno RSC ejecuta Server Components y genera un stream
- El entorno SSR consume ese stream y produce HTML
- El entorno Client hidrata el HTML en el navegador
Esta separación es manejada por @vitejs/plugin-rsc y es transparente para el desarrollador.
Configuración en vite.config.ts
Para App Router, el plugin RSC configura los ambientes automáticamente:
import { defineConfig } from "vite";
import vinext from "vinext";
export default defineConfig({
plugins: [vinext()],
});
Si usas Cloudflare Workers, especifica los ambientes:
import { defineConfig } from "vite";
import vinext from "vinext";
import { cloudflare } from "@cloudflare/vite-plugin";
export default defineConfig({
plugins: [
vinext(),
cloudflare({
viteEnvironment: {
name: "rsc",
childEnvironments: ["ssr"],
},
}),
],
});
Patrones comunes
Pasar datos de Server a Client Component
// app/page.tsx (Server Component)
import { UserProfile } from "./components/user-profile";
export default async function Page() {
const user = await getUser(); // Fetch en servidor
// Pasa datos serializables como props
return <UserProfile name={user.name} email={user.email} />;
}
// app/components/user-profile.tsx (Client Component)
"use client";
export function UserProfile({ name, email }: { name: string; email: string }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<button onClick={() => navigator.clipboard.writeText(email)}>
Copiar email
</button>
</div>
);
}
Composición: Client Component con Server Component hijo
// app/components/tabs.tsx (Client Component)
"use client";
import { useState } from "react";
export function Tabs({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(0);
return (
<div>
<div className="tab-buttons">
<button onClick={() => setActive(0)}>Tab 1</button>
<button onClick={() => setActive(1)}>Tab 2</button>
</div>
<div>{children}</div>
</div>
);
}
// app/page.tsx (Server Component)
import { Tabs } from "./components/tabs";
export default async function Page() {
const data = await fetchData(); // Ejecuta en servidor
return (
<Tabs>
<p>{data.content}</p> {/* Renderizado en servidor */}
</Tabs>
);
}
Siguiente paso
En el siguiente capítulo aprenderemos Server Actions: cómo manejar formularios, mutaciones y revalidación de datos.