Volver al blog

React 19 Server Components: Guía Práctico Para Desarrolladores

Hola HaWkers, React 19 trajo Server Components como una feature estable, cambiando fundamentalmente cómo pensamos sobre renderización en React. Si aún no entendiste bien cómo funcionan o cuándo usar, esta guía práctica va a aclarar todo.

¿Ya te preguntaste por qué tus bundles JavaScript están tan grandes o por qué la performance inicial de tu aplicación es lenta? Server Components pueden ser la respuesta.

Qué Son Server Components

Server Components son componentes React que ejecutan exclusivamente en el servidor. Diferente de SSR tradicional, ellos nunca envían JavaScript para el cliente.

Diferencia Fundamental

Comparativo de abordajes:

Aspecto Client Components Server Components
Dónde ejecuta Browser Servidor
JavaScript en el cliente No
Acceso a base de datos Vía API Directo
Estado y eventos useState, onClick No disponible
Performance inicial Más lento Más rápido

💡 Contexto: Server Components no sustituyen Client Components. Ellos trabajan juntos, cada uno en lo que hace mejor.

Cómo Funcionan en la Práctica

Vamos a ver ejemplos concretos de Server y Client Components:

Server Component Básico

// app/posts/page.tsx (Server Component por default)
import { db } from '@/lib/database';

// Este componente NUNCA va para el browser
async function PostsPage() {
  // Acceso directo a la base de datos
  const posts = await db.posts.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <div className="posts-container">
      <h1>Últimos Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <time>{new Date(post.createdAt).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  );
}

export default PostsPage;

Client Component

// components/LikeButton.tsx
'use client'; // Marcador obligatorio

import { useState, useTransition } from 'react';
import { likePost } from '@/actions/posts';

interface LikeButtonProps {
  postId: string;
  initialLikes: number;
}

export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, startTransition] = useTransition();

  const handleLike = () => {
    startTransition(async () => {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    });
  };

  return (
    <button
      onClick={handleLike}
      disabled={isPending}
      className="like-button"
    >
      {isPending ? '...' : `❤️ ${likes}`}
    </button>
  );
}

Combinando Server y Client

// app/posts/[id]/page.tsx
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';

async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.posts.findUnique({
    where: { id: params.id },
    include: { author: true, comments: true },
  });

  if (!post) {
    return <div>Post no encontrado</div>;
  }

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <span>Por {post.author.name}</span>
      </header>

      <div className="content">
        {post.content}
      </div>

      {/* Client Component con interactividad */}
      <LikeButton postId={post.id} initialLikes={post.likes} />

      {/* Otro Client Component */}
      <CommentSection
        postId={post.id}
        initialComments={post.comments}
      />
    </article>
  );
}

export default PostPage;

Patrones de Composición

La clave para usar Server Components efectivamente es entender patrones de composición:

1. Pasando Server Components como Children

// components/ClientWrapper.tsx
'use client';

import { useState } from 'react';

interface ClientWrapperProps {
  children: React.ReactNode;
}

export function ClientWrapper({ children }: ClientWrapperProps) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div className={isExpanded ? 'expanded' : 'collapsed'}>
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Recoger' : 'Expandir'}
      </button>
      {isExpanded && children}
    </div>
  );
}

// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper';
import { ExpensiveServerComponent } from '@/components/ExpensiveServerComponent';

export default function Page() {
  return (
    <ClientWrapper>
      {/* Este Server Component es pasado como children */}
      <ExpensiveServerComponent />
    </ClientWrapper>
  );
}

2. Slots Pattern

// components/Layout.tsx
'use client';

import { useState } from 'react';

interface LayoutProps {
  header: React.ReactNode;
  sidebar: React.ReactNode;
  content: React.ReactNode;
}

export function Layout({ header, sidebar, content }: LayoutProps) {
  const [sidebarOpen, setSidebarOpen] = useState(true);

  return (
    <div className="layout">
      <header>{header}</header>
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>
        Toggle Sidebar
      </button>
      {sidebarOpen && <aside>{sidebar}</aside>}
      <main>{content}</main>
    </div>
  );
}

// app/dashboard/page.tsx
import { Layout } from '@/components/Layout';
import { UserStats } from '@/components/UserStats';
import { Navigation } from '@/components/Navigation';
import { DashboardContent } from '@/components/DashboardContent';

export default function DashboardPage() {
  return (
    <Layout
      header={<Navigation />}
      sidebar={<UserStats />}
      content={<DashboardContent />}
    />
  );
}

Server Actions

React 19 introdujo Server Actions para mutations de forma integrada:

Creando Server Actions

// actions/posts.ts
'use server';

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

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  const rawData = {
    title: formData.get('title'),
    content: formData.get('content'),
  };

  const validatedData = PostSchema.safeParse(rawData);

  if (!validatedData.success) {
    return { error: validatedData.error.flatten().fieldErrors };
  }

  try {
    const post = await db.posts.create({
      data: validatedData.data,
    });

    revalidatePath('/posts');
    return { success: true, postId: post.id };
  } catch (error) {
    return { error: 'Fallo al crear post' };
  }
}

export async function likePost(postId: string) {
  const post = await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  revalidatePath(`/posts/${postId}`);
  return post.likes;
}

export async function deletePost(postId: string) {
  await db.posts.delete({ where: { id: postId } });
  revalidatePath('/posts');
}

Usando Server Actions en Forms

// components/CreatePostForm.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '@/actions/posts';

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Título</label>
        <input
          id="title"
          name="title"
          type="text"
          required
          disabled={isPending}
        />
        {state?.error?.title && (
          <span className="error">{state.error.title}</span>
        )}
      </div>

      <div>
        <label htmlFor="content">Contenido</label>
        <textarea
          id="content"
          name="content"
          rows={10}
          required
          disabled={isPending}
        />
        {state?.error?.content && (
          <span className="error">{state.error.content}</span>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creando...' : 'Crear Post'}
      </button>

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

Cuándo Usar Cada Tipo

Guía práctico para decidir entre Server y Client Components:

Use Server Components Para

Casos ideales:

  • Fetch de datos de la base
  • Acceso a APIs privadas
  • Componentes sin interactividad
  • Contenido estático o semi-estático
  • Reducción de bundle size
// Ejemplos de Server Components
// - Listados de datos
// - Headers y footers
// - Navegación estática
// - Contenido de páginas
// - Componentes de layout

async function ProductList() {
  const products = await db.products.findMany();

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </li>
      ))}
    </ul>
  );
}

Use Client Components Para

Casos ideales:

  • Interactividad (onClick, onChange)
  • Estado local (useState)
  • Efectos (useEffect)
  • Browser APIs (localStorage, geolocation)
  • Bibliotecas client-only
// Ejemplos de Client Components
// - Formularios
// - Modales y dialogs
// - Carruseles y sliders
// - Tooltips y dropdowns
// - Componentes con animación

'use client';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const search = async () => {
      if (query.length > 2) {
        const data = await fetch(`/api/search?q=${query}`);
        setResults(await data.json());
      }
    };
    const timer = setTimeout(search, 300);
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Buscar..."
      />
      <ul>
        {results.map(r => <li key={r.id}>{r.title}</li>)}
      </ul>
    </div>
  );
}

Errores Comunes y Soluciones

Evita estos problemas frecuentes:

1. Importar Client Component sin 'use client'

// ❌ Errado: usando hooks sin 'use client'
import { useState } from 'react';

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

// ✅ Correcto: añadir 'use client'
'use client';

import { useState } from 'react';

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

2. Pasar Funciones No Serializables

// ❌ Errado: pasando función de Server para Client
async function ServerComponent() {
  const handleClick = () => console.log('clicked'); // ¡No serializable!

  return <ClientButton onClick={handleClick} />; // ¡Error!
}

// ✅ Correcto: usar Server Action
async function ServerComponent() {
  return <ClientButton postId="123" />;
}

// Client Component llama la action
'use client';
import { likePost } from '@/actions/posts';

function ClientButton({ postId }: { postId: string }) {
  return <button onClick={() => likePost(postId)}>Like</button>;
}

Conclusión

React 19 Server Components representan un cambio fundamental en cómo construimos aplicaciones React. Al ejecutar componentes en el servidor y enviar apenas HTML para el cliente, conseguimos aplicaciones más rápidas, bundles menores, y acceso directo a recursos del servidor.

La clave es entender que Server y Client Components trabajan juntos. Usa Server Components para buscar datos y renderizar contenido estático, y Client Components para interactividad. Con esa división clara, tus aplicaciones serán más performáticas y más fáciles de mantener.

Si quieres profundizarte en React y desarrollo frontend moderno, recomiendo que revises otro artículo: React vs Vue vs Svelte en 2025 donde vas a descubrir alternativas interesantes a React.

¡Vamos a por ello! 🦅

Comentarios (0)

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

Añadir comentarios