SSR y React Server Components

Por: Artiko
vinextssrrscserver-componentsstreaming

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:

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 ComponentClient 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:

  1. El servidor envía el shell HTML con los loading states
  2. A medida que los datos se resuelven, el contenido reemplaza los placeholders
  3. 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:

  1. El servidor envía layout.tsx + loading.tsx como HTML inicial
  2. Mientras, dashboard/page.tsx resuelve sus datos
  3. El HTML del page.tsx se 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:

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.