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 | Sí | 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.

