Volver al blog

React Server Components: Guía Completa y Práctica Para 2025

Hola HaWkers, React Server Components (RSC) dejaron de ser una feature experimental para convertirse en el estándar de facto en el desarrollo React moderno. Con Next.js 15 consolidando el modelo y otros frameworks adoptando la tecnología, entender RSC profundamente es esencial para cualquier desarrollador frontend en 2025.

En esta guía, vamos a explorar no apenas el "cómo", sino principalmente el "cuándo" y el "por qué" de usar Server Components.

Qué Son Server Components

React Server Components son componentes que ejecutan exclusivamente en el servidor, nunca siendo enviados para el navegador del usuario. Esto representa un cambio fundamental en el modelo mental de cómo construimos aplicaciones React.

Modelo Mental Tradicional vs RSC

React Tradicional (Client-Side Rendering):

  1. Usuario accede a la página
  2. Servidor envía HTML mínimo + JavaScript
  3. JavaScript descarga, parsea y ejecuta
  4. React "hidrata" la página, tornándola interactiva
  5. Datos son buscados via API (useEffect, React Query, etc.)
  6. Componentes re-renderizan con datos

React Server Components:

  1. Usuario accede a la página
  2. Servidor ejecuta componentes y busca datos
  3. Servidor envía HTML renderizado + JavaScript mínimo
  4. Apenas componentes interactivos son hidratados
  5. Página ya llega lista y funcional

La Gran Diferencia

// ❌ Componente Cliente Tradicional
'use client'

import { useState, useEffect } from 'react';

export function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Fetch acontece en el CLIENTE después del render inicial
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <ProductSkeleton />;

  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  );
}
// ✅ Server Component
// No necesita 'use client' - es server por defecto

import { db } from '@/lib/database';

export async function ProductList() {
  // Fetch acontece en el SERVIDOR antes de enviar HTML
  const products = await db.products.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' }
  });

  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  );
}

Beneficios Concretos

Vamos a cuantificar los beneficios de Server Components con ejemplos reales.

Reducción de Bundle Size

// Análisis de bundle típico

// Aplicación e-commerce con React tradicional
const clientBundleTraditional = {
  react: '42kb',
  reactDom: '130kb',
  reactQuery: '35kb',
  axios: '14kb',
  zustand: '8kb',
  dateFormats: '72kb', // date-fns, moment, etc.
  markdown: '45kb',    // para renderizar descripciones
  syntax: '180kb',     // highlight.js para code blocks
  total: '526kb gzipped'
};

// Misma aplicación con RSC
const clientBundleRSC = {
  react: '42kb',
  reactDom: '130kb',
  // reactQuery: no necesita para data fetching
  // axios: no necesita, fetch en servidor
  zustand: '8kb',      // aún necesario para estado cliente
  // dateFormats: renderizado en servidor
  // markdown: renderizado en servidor
  // syntax: renderizado en servidor
  total: '180kb gzipped' // 66% menor!
};

Performance de Carga

// Métricas reales de una aplicación migrada para RSC

// Antes (CSR)
const metricsCSR = {
  TTFB: '180ms',
  FCP: '1.8s',
  LCP: '3.2s',
  TTI: '4.1s',
  CLS: 0.12,
  bundleSize: '450kb'
};

// Después (RSC)
const metricsRSC = {
  TTFB: '220ms',      // Ligeramente mayor (renderización servidor)
  FCP: '0.9s',        // 50% más rápido
  LCP: '1.4s',        // 56% más rápido
  TTI: '1.8s',        // 56% más rápido
  CLS: 0.02,          // 83% mejor (menos layout shift)
  bundleSize: '180kb' // 60% menor
};

Cuándo Usar Server vs Client Components

La decisión entre Server y Client Components debe ser basada en criterios claros.

Usa Server Components Cuando

// ✅ Fetching de datos
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findUnique({ where: { id: userId } });
  return <ProfileCard user={user} />;
}

// ✅ Acceso a recursos del servidor
async function ConfigPanel() {
  const config = await readFile('./config.json', 'utf-8');
  return <ConfigDisplay config={JSON.parse(config)} />;
}

// ✅ Renderización de contenido pesado
import { marked } from 'marked';
import hljs from 'highlight.js';

async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);
  const html = marked(post.content, {
    highlight: (code, lang) => hljs.highlight(code, { language: lang }).value
  });

  return <article dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ Datos sensibles
async function AdminDashboard() {
  // Secrets nunca van para el cliente
  const analytics = await fetchAnalytics(process.env.ANALYTICS_SECRET);
  return <DashboardCharts data={analytics} />;
}

Usa Client Components Cuando

'use client'

// ✅ Interactividad con estado
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clics: {count}
    </button>
  );
}

// ✅ Efectos y lifecycle
import { useEffect } from 'react';

export function AnalyticsTracker() {
  useEffect(() => {
    trackPageView();
  }, []);

  return null;
}

// ✅ Event handlers
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
  return (
    <input
      type="search"
      onChange={(e) => onSearch(e.target.value)}
      placeholder="Buscar..."
    />
  );
}

// ✅ APIs del navegador
export function LocationDisplay() {
  const [coords, setCoords] = useState<GeolocationCoordinates | null>(null);

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      (pos) => setCoords(pos.coords)
    );
  }, []);

  return coords ? (
    <span>Lat: {coords.latitude}, Lng: {coords.longitude}</span>
  ) : null;
}

// ✅ Hooks customizados con estado
import { useLocalStorage } from '@/hooks/useLocalStorage';

export function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Tema: {theme}
    </button>
  );
}

Patrones de Composición

El arte de RSC está en componer Server y Client Components de forma eficiente.

El Patrón "Wrapper"

// Server Component que envuelve Client Component
// page.tsx (Server Component)

import { ProductFilters } from './ProductFilters'; // Client
import { db } from '@/lib/database';

export default async function ProductsPage() {
  // Datos buscados en servidor
  const categories = await db.categories.findMany();
  const brands = await db.brands.findMany();

  return (
    <div>
      <h1>Productos</h1>
      {/* Client Component recibe datos del servidor como props */}
      <ProductFilters
        categories={categories}
        brands={brands}
      />
      {/* Resto de la página es Server Component */}
      <ProductGrid />
    </div>
  );
}
// ProductFilters.tsx
'use client'

import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

interface Props {
  categories: Category[];
  brands: Brand[];
}

export function ProductFilters({ categories, brands }: Props) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [selectedCategory, setSelectedCategory] = useState(
    searchParams.get('category') ?? ''
  );

  function handleCategoryChange(categoryId: string) {
    setSelectedCategory(categoryId);
    const params = new URLSearchParams(searchParams);
    params.set('category', categoryId);
    router.push(`/products?${params.toString()}`);
  }

  return (
    <aside>
      <h3>Filtros</h3>
      <select
        value={selectedCategory}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="">Todas categorías</option>
        {categories.map(cat => (
          <option key={cat.id} value={cat.id}>{cat.name}</option>
        ))}
      </select>
      {/* Más filtros... */}
    </aside>
  );
}

El Patrón "Slot"

// Server Component que acepta Client Components como children

// Modal.tsx (Server Component)
interface ModalProps {
  title: string;
  children: React.ReactNode; // Puede ser Client o Server
}

export function Modal({ title, children }: ModalProps) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <h2>{title}</h2>
        {children}
      </div>
    </div>
  );
}

// Uso en page.tsx
import { Modal } from './Modal';
import { InteractiveForm } from './InteractiveForm'; // Client Component

export default function Page() {
  return (
    <Modal title="Nuevo Producto">
      <InteractiveForm /> {/* Client Component dentro de Server */}
    </Modal>
  );
}

El Patrón "Island"

// page.tsx - Mayoritariamente Server con "islas" de interactividad

import { Suspense } from 'react';
import { LikeButton } from './LikeButton';        // Client
import { CommentSection } from './CommentSection'; // Client
import { ShareMenu } from './ShareMenu';           // Client

export default async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);
  const author = await getAuthor(post.authorId);

  return (
    <article>
      {/* Contenido estático - Server */}
      <header>
        <h1>{post.title}</h1>
        <AuthorCard author={author} />
        <time>{formatDate(post.publishedAt)}</time>
      </header>

      {/* Contenido renderizado - Server */}
      <div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />

      {/* Islas de interactividad - Client */}
      <footer>
        <LikeButton postId={post.id} initialLikes={post.likes} />
        <ShareMenu url={`/blog/${slug}`} title={post.title} />
      </footer>

      {/* Sección interactiva con loading state */}
      <Suspense fallback={<CommentSkeleton />}>
        <CommentSection postId={post.id} />
      </Suspense>
    </article>
  );
}

Streaming y Suspense

RSC habilitan streaming de HTML, mejorando significativamente la experiencia percibida.

Streaming Básico

// page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Renderiza inmediatamente */}
      <QuickStats />

      {/* Streamea cuando listo */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart /> {/* Async Server Component */}
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders /> {/* Async Server Component */}
      </Suspense>

      <Suspense fallback={<ListSkeleton />}>
        <TopProducts /> {/* Async Server Component */}
      </Suspense>
    </div>
  );
}

// Cada componente async busca datos independientemente
async function RevenueChart() {
  const data = await fetchRevenueData(); // Puede demorar 2s
  return <Chart data={data} />;
}

async function RecentOrders() {
  const orders = await fetchRecentOrders(); // Puede demorar 1s
  return <OrdersTable orders={orders} />;
}

async function TopProducts() {
  const products = await fetchTopProducts(); // Puede demorar 500ms
  return <ProductsList products={products} />;
}

Parallel Data Fetching

// ✅ Correcto: Fetches en paralelo
async function Dashboard() {
  // Inicia todos los fetches simultáneamente
  const revenuePromise = fetchRevenue();
  const ordersPromise = fetchOrders();
  const productsPromise = fetchProducts();

  // Aguarda todos
  const [revenue, orders, products] = await Promise.all([
    revenuePromise,
    ordersPromise,
    productsPromise
  ]);

  return (
    <div>
      <RevenueChart data={revenue} />
      <OrdersTable data={orders} />
      <ProductsList data={products} />
    </div>
  );
}

// ❌ Incorrecto: Fetches secuenciales (waterfall)
async function DashboardSlow() {
  const revenue = await fetchRevenue();    // Espera 1s
  const orders = await fetchOrders();      // Después espera más 1s
  const products = await fetchProducts();  // Después más 500ms
  // Total: 2.5s

  return (/* ... */);
}

// Con Promise.all: 1s (el más lento)
// Sin Promise.all: 2.5s (suma de todos)

Server Actions

Server Actions complementan RSC permitiendo mutaciones de forma elegante.

Básico de Server Actions

// actions.ts
'use server'

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';

export async function createProduct(formData: FormData) {
  const name = formData.get('name') as string;
  const price = parseFloat(formData.get('price') as string);

  await db.products.create({
    data: { name, price }
  });

  revalidatePath('/products');
}

export async function deleteProduct(productId: string) {
  await db.products.delete({
    where: { id: productId }
  });

  revalidatePath('/products');
}
// ProductForm.tsx - Puede ser Server Component!
import { createProduct } from './actions';

export function ProductForm() {
  return (
    <form action={createProduct}>
      <input name="name" placeholder="Nombre del producto" required />
      <input name="price" type="number" step="0.01" required />
      <button type="submit">Crear Producto</button>
    </form>
  );
}

Server Actions con Validación

// actions.ts
'use server'

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const ProductSchema = z.object({
  name: z.string().min(3).max(100),
  price: z.number().positive(),
  description: z.string().optional(),
  categoryId: z.string().uuid()
});

type ActionResult = {
  success: boolean;
  error?: string;
  data?: any;
};

export async function createProduct(formData: FormData): Promise<ActionResult> {
  const rawData = {
    name: formData.get('name'),
    price: parseFloat(formData.get('price') as string),
    description: formData.get('description'),
    categoryId: formData.get('categoryId')
  };

  const validation = ProductSchema.safeParse(rawData);

  if (!validation.success) {
    return {
      success: false,
      error: validation.error.errors[0].message
    };
  }

  try {
    const product = await db.products.create({
      data: validation.data
    });

    revalidatePath('/products');

    return {
      success: true,
      data: product
    };
  } catch (error) {
    return {
      success: false,
      error: 'Error al crear producto'
    };
  }
}
// ProductForm.tsx
'use client'

import { useFormState, useFormStatus } from 'react-dom';
import { createProduct } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creando...' : 'Crear Producto'}
    </button>
  );
}

export function ProductForm() {
  const [state, formAction] = useFormState(createProduct, {
    success: false
  });

  return (
    <form action={formAction}>
      {state.error && (
        <div className="error">{state.error}</div>
      )}

      <input name="name" placeholder="Nombre" required />
      <input name="price" type="number" step="0.01" required />
      <textarea name="description" placeholder="Descripción" />

      <SubmitButton />

      {state.success && (
        <div className="success">Producto creado con éxito!</div>
      )}
    </form>
  );
}

Errores Comunes y Cómo Evitar

Aprende de los errores más frecuentes al trabajar con RSC.

Error 1: Importar Client Component sin 'use client'

// ❌ Error común
// Button.tsx - Olvidó 'use client'
import { useState } from 'react';

export function Button() {
  const [clicked, setClicked] = useState(false);
  // Error: useState no funciona en Server Components
}

// ✅ Correcto
// Button.tsx
'use client'

import { useState } from 'react';

export function Button() {
  const [clicked, setClicked] = useState(false);
  // Funciona!
}

Error 2: Pasar Funciones Como Props Para Client Components

// ❌ Error
// page.tsx (Server Component)
export default function Page() {
  function handleClick() {
    console.log('clicked');
  }

  // Error: funciones no son serializables
  return <ClientButton onClick={handleClick} />;
}

// ✅ Correcto - Usa Server Actions
// page.tsx
import { handleAction } from './actions';

export default function Page() {
  return <ClientButton action={handleAction} />;
}

// actions.ts
'use server'
export async function handleAction() {
  console.log('action executed');
}

Error 3: Usar Hooks en Server Components

// ❌ Error
// ServerComponent.tsx
import { useRouter } from 'next/navigation';

export function ServerComponent() {
  const router = useRouter(); // Error: hooks no funcionan aquí
}

// ✅ Correcto - Mueve para Client Component
// Navegación via redirect en Server Components
import { redirect } from 'next/navigation';

export async function ServerComponent() {
  const shouldRedirect = await checkCondition();
  if (shouldRedirect) {
    redirect('/other-page');
  }
}

Migración de Proyectos Existentes

Estrategias para migrar gradualmente para RSC.

Abordaje Incremental

// 1. Identifica componentes "hoja" que no necesitan interactividad

// Antes: Client Component innecesario
'use client'
export function ProductCard({ product }) {
  return (
    <div className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
}

// Después: Server Component (remueve 'use client')
export function ProductCard({ product }) {
  return (
    <div className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{formatCurrency(product.price)}</p> {/* Puede usar libs pesadas */}
    </div>
  );
}

Checklist de Migración

## Migración para RSC

### Fase 1: Análisis
- [ ] Identificar componentes sin estado/efectos
- [ ] Mapear dependencias de cada componente
- [ ] Identificar data fetching patterns actuales

### Fase 2: Preparación
- [ ] Actualizar Next.js para versión 13.4+
- [ ] Configurar App Router
- [ ] Crear estructura de carpetas /app

### Fase 3: Migración Gradual
- [ ] Convertir layouts para Server Components
- [ ] Mover data fetching para el servidor
- [ ] Marcar componentes interactivos con 'use client'
- [ ] Sustituir useEffect + fetch por async components
- [ ] Implementar Suspense boundaries

### Fase 4: Optimización
- [ ] Implementar streaming donde apropiado
- [ ] Agregar loading.tsx para rutas
- [ ] Configurar cache y revalidación
- [ ] Implementar Server Actions para forms

Conclusión

React Server Components representan la mayor evolución en la arquitectura React desde los Hooks. Ellos resuelven problemas reales de performance y experiencia del usuario que desarrolladores enfrentan hace años.

El secreto para dominar RSC está en entender claramente la división de responsabilidades: Server Components para datos y renderización pesada, Client Components para interactividad. Con esa mentalidad, construirás aplicaciones más rápidas, más simples y más escalables.

Si quieres profundizar tus conocimientos en React moderno, te recomiendo que eches un vistazo a otro artículo: React Hooks Avanzados: Patterns y Optimización donde vas a descubrir técnicas que complementan el uso de Server Components.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios