React 19 Server Components: Guía Práctica Completa en 2025
Hola HaWkers, React Server Components (RSC) cambiaron fundamentalmente cómo construimos aplicaciones React. En 2025, con React 19 estable, RSC son el patrón predeterminado en Next.js y otros frameworks.
¿Aún estás confundido sobre cuándo usar Server vs Client Components? Esta guía práctica va a clarificar todo con ejemplos reales.
Qué Son React Server Components
Server Components son componentes que se ejecutan exclusivamente en el servidor. Ellos pueden:
- Acceder directamente a bases de datos
- Leer archivos del sistema
- Hacer fetch de APIs sin exponer claves
- Enviar solo HTML para el cliente (zero JavaScript)
// Este componente NUNCA va para el navegador
async function UserProfile({ userId }: { userId: string }) {
// Acceso directo a la base de datos
const user = await db.users.findUnique({
where: { id: userId },
include: { posts: true },
});
return (
<div className="profile">
<h1>{user.name}</h1>
<p>{user.bio}</p>
<PostsList posts={user.posts} />
</div>
);
}
Server vs Client Components
Cuándo Usar Cada Uno
| Necesitas... | Usa |
|---|---|
| Fetch de datos | Server Component |
| Acceso a backend (DB, filesystem) | Server Component |
| Secretos/API keys | Server Component |
| Reducir bundle size | Server Component |
| useState, useEffect | Client Component |
| Event listeners (onClick, etc) | Client Component |
| APIs del browser (localStorage, etc) | Client Component |
| Hooks customizados con estado | Client Component |
Regla de Oro
Server Component = Datos + Presentación estática
Client Component = Interactividad
// page.tsx (Server Component)
import { ClientButton } from './client-button';
export default async function Page() {
const data = await fetchData(); // Ejecuta en el servidor
return (
<div>
<h1>{data.title}</h1> {/* Estático */}
<ClientButton /> {/* Interactivo */}
</div>
);
}// client-button.tsx (Client Component)
'use client';
import { useState } from 'react';
export function ClientButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicado {count} veces
</button>
);
}
Patrones de Composición
1. Server Component como Container
// users-page.tsx (Server)
import { UsersList } from './users-list';
import { UserFilters } from './user-filters';
export default async function UsersPage() {
const users = await db.users.findMany();
return (
<div className="users-page">
<h1>Usuarios</h1>
<UserFilters /> {/* Client - interactividad */}
<UsersList users={users} /> {/* Server - solo render */}
</div>
);
}// user-filters.tsx (Client)
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export function UserFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const handleFilterChange = (filter: string) => {
const params = new URLSearchParams(searchParams);
params.set('filter', filter);
router.push(`?${params.toString()}`);
};
return (
<select onChange={(e) => handleFilterChange(e.target.value)}>
<option value="all">Todos</option>
<option value="active">Activos</option>
<option value="inactive">Inactivos</option>
</select>
);
}2. Pasando Server Data para Client Component
// product-page.tsx (Server)
import { ProductDetails } from './product-details';
import { AddToCartButton } from './add-to-cart-button';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.products.findUnique({
where: { id: params.id },
});
return (
<div>
<ProductDetails product={product} /> {/* Server */}
<AddToCartButton
productId={product.id}
price={product.price}
/> {/* Client con datos del server */}
</div>
);
}// add-to-cart-button.tsx (Client)
'use client';
import { useState } from 'react';
import { addToCart } from './actions';
interface Props {
productId: string;
price: number;
}
export function AddToCartButton({ productId, price }: Props) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Agregando...' : `Agregar por ${price}€`}
</button>
);
}3. Children Pattern
// modal-wrapper.tsx (Client)
'use client';
import { useState } from 'react';
export function ModalWrapper({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
if (!isOpen) {
return <button onClick={() => setIsOpen(true)}>Abrir Modal</button>;
}
return (
<dialog open>
{children} {/* Server Component puede ser children */}
<button onClick={() => setIsOpen(false)}>Cerrar</button>
</dialog>
);
}// page.tsx (Server)
import { ModalWrapper } from './modal-wrapper';
import { UserProfile } from './user-profile';
export default async function Page() {
return (
<ModalWrapper>
{/* Server Component como children de Client Component */}
<UserProfile userId="123" />
</ModalWrapper>
);
}
Server Actions
Server Actions permiten llamar funciones del servidor directamente desde el cliente.
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validación
if (!title || title.length < 3) {
return { error: 'Título muy corto' };
}
// Crear en el banco
const post = await db.posts.create({
data: { title, content },
});
// Revalidar cache
revalidatePath('/posts');
// Redirigir
redirect(`/posts/${post.id}`);
}
export async function deletePost(id: string) {
await db.posts.delete({ where: { id } });
revalidatePath('/posts');
}
export async function likePost(id: string) {
await db.posts.update({
where: { id },
data: { likes: { increment: 1 } },
});
revalidatePath(`/posts/${id}`);
}Usando en Forms
// create-post-form.tsx (Client)
'use client';
import { useFormStatus } from 'react-dom';
import { createPost } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creando...' : 'Crear Post'}
</button>
);
}
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Título" required />
<textarea name="content" placeholder="Contenido" required />
<SubmitButton />
</form>
);
}Usando con useActionState
// form-with-state.tsx (Client)
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
const initialState = { error: null };
export function FormWithState() {
const [state, formAction, pending] = useActionState(
createPost,
initialState
);
return (
<form action={formAction}>
<input name="title" placeholder="Título" />
{state?.error && <p className="error">{state.error}</p>}
<button disabled={pending}>
{pending ? 'Enviando...' : 'Enviar'}
</button>
</form>
);
}
Optimizaciones de Performance
1. Streaming con Suspense
// page.tsx (Server)
import { Suspense } from 'react';
import { SlowComponent } from './slow-component';
import { FastComponent } from './fast-component';
export default function Page() {
return (
<div>
{/* Renderiza inmediatamente */}
<FastComponent />
{/* Streama cuando estiver listo */}
<Suspense fallback={<LoadingSpinner />}>
<SlowComponent />
</Suspense>
{/* Múltiplos Suspense para streaming paralelo */}
<div className="grid">
<Suspense fallback={<CardSkeleton />}>
<RecommendedPosts />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<PopularPosts />
</Suspense>
</div>
</div>
);
}2. Parallel Data Fetching
// page.tsx (Server)
export default async function DashboardPage() {
// MAL: Secuencial
// const user = await fetchUser();
// const posts = await fetchPosts();
// const analytics = await fetchAnalytics();
// BIEN: Paralelo
const [user, posts, analytics] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchAnalytics(),
]);
return (
<div>
<UserHeader user={user} />
<PostsGrid posts={posts} />
<AnalyticsChart data={analytics} />
</div>
);
}3. Preloading Data
// lib/data.ts
import { cache } from 'react';
import 'server-only';
// Cache automático para requests duplicados
export const getUser = cache(async (id: string) => {
const user = await db.users.findUnique({ where: { id } });
return user;
});
// Preload function
export const preloadUser = (id: string) => {
void getUser(id);
};// page.tsx
import { getUser, preloadUser } from '@/lib/data';
export default async function UserPage({ params }: { params: { id: string } }) {
// Iniciar preload inmediatamente
preloadUser(params.id);
// Hacer otras cosas mientras carga
const otherData = await fetchOtherData();
// Usar los datos (ya pueden estar en cache)
const user = await getUser(params.id);
return <Profile user={user} />;
}
Patrones Comunes
Data Table con Sorting/Filtering
// data-table-page.tsx (Server)
import { DataTableClient } from './data-table-client';
interface Props {
searchParams: {
page?: string;
sort?: string;
filter?: string;
};
}
export default async function DataTablePage({ searchParams }: Props) {
const page = Number(searchParams.page) || 1;
const sort = searchParams.sort || 'createdAt';
const filter = searchParams.filter || '';
const { data, total } = await db.items.findMany({
where: filter ? { name: { contains: filter } } : {},
orderBy: { [sort]: 'desc' },
skip: (page - 1) * 10,
take: 10,
});
return (
<DataTableClient
data={data}
total={total}
currentPage={page}
currentSort={sort}
currentFilter={filter}
/>
);
}// data-table-client.tsx (Client)
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
interface Props {
data: Item[];
total: number;
currentPage: number;
currentSort: string;
currentFilter: string;
}
export function DataTableClient({
data,
total,
currentPage,
currentSort,
currentFilter,
}: Props) {
const router = useRouter();
const searchParams = useSearchParams();
const updateParams = (updates: Record<string, string>) => {
const params = new URLSearchParams(searchParams);
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
});
router.push(`?${params.toString()}`);
};
return (
<div>
<input
type="search"
defaultValue={currentFilter}
onChange={(e) => updateParams({ filter: e.target.value, page: '1' })}
placeholder="Filtrar..."
/>
<table>
<thead>
<tr>
<th onClick={() => updateParams({ sort: 'name' })}>
Nombre {currentSort === 'name' && '↓'}
</th>
<th onClick={() => updateParams({ sort: 'createdAt' })}>
Fecha {currentSort === 'createdAt' && '↓'}
</th>
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.createdAt.toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(total / 10)}
onPageChange={(page) => updateParams({ page: String(page) })}
/>
</div>
);
}Errores Comunes
1. Importar Client Component sin 'use client'
// ERROR: useState en Server Component
import { useState } from 'react'; // Error!
export default function Page() {
const [count, setCount] = useState(0); // Error!
return <div>{count}</div>;
}2. Pasar Funciones para Client Components
// ERROR: Funciones no son serializables
export default function Page() {
const handleClick = () => console.log('clicked');
return <ClientButton onClick={handleClick} />; // Error!
}
// CORRECTO: Usar Server Actions
import { serverAction } from './actions';
export default function Page() {
return <ClientButton action={serverAction} />; // OK
}3. Acceder a APIs del Browser en Server
// ERROR
export default function Page() {
const width = window.innerWidth; // Error! window no existe en server
return <div>{width}</div>;
}
// CORRECTO: Mover para Client Component
'use client';
export function WindowWidth() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>{width}</div>;
}Conclusión
React Server Components representan un cambio de paradigma en cómo construimos aplicaciones React. La clave es:
- Default para Server: Comienza con Server Components
- Client cuando necesario: Agrega 'use client' solo para interactividad
- Composición inteligente: Combina Server y Client estratégicamente
- Server Actions para mutaciones: Simplifica formularios y acciones
Si quieres profundizar en las herramientas modernas de React, recomiendo que veas otro artículo: Svelte vs Vue vs React en 2025 donde vas a descubrir cómo React se compara con otros frameworks.

