React Server Components (RSC) representan el mayor cambio arquitectonico desde los hooks.
Que son los RSC?
Componentes que se renderizan en el servidor.
Tres tipos de componentes en el modelo RSC.
Server vs Client
| Aspecto | Server Component | Client Component |
|---|---|---|
| Por defecto | Si | No |
| Directiva | Ninguna | "use client" |
| JS cliente | Ninguno | Si |
| useState/useEffect | No disponible | Disponible |
| Eventos | No disponible | Disponible |
| Acceso DB | Si | No |
| Sistema de archivos | Si | No |
| Variables servidor | Si | No |
| Componente async | Si | No |
| Tamano bundle | Cero | Aumenta |
Arbol de decision
Should this component be a Server or Client Component?
Does it need useState, useEffect, or useRef?
├── YES → Client Component ("use client")
└── NO ↓
Does it need event handlers (onClick, onChange, onSubmit)?
├── YES → Client Component ("use client")
└── NO ↓
Does it use browser-only APIs (window, localStorage, navigator)?
├── YES → Client Component ("use client")
└── NO ↓
Does it use third-party libraries that require hooks or browser APIs?
├── YES → Client Component ("use client")
└── NO ↓
Does it fetch data or access databases/file system?
├── YES → Server Component (default) ← BEST CHOICE
└── NO → Server Component (default) ← still the defaultPatrones RSC
Patron 1: Carga de datos servidor
Carga directa con async/await.
// app/users/page.tsx — Server Component (default, no directive needed)
import { db } from '@/lib/database';
// This component is async — only possible in Server Components
export default async function UsersPage() {
// Direct database query — this code NEVER reaches the client
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
});
return (
<div>
<h1>Users ({users.length})</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
</div>
);
}
// Benefits:
// - Zero JavaScript sent to client for this component
// - No loading states needed (data is available before render)
// - Database credentials never exposed to the browser
// - No useEffect + fetch + useState dancePatron 2: Composicion
Server puede renderizar Client.
// components/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
return (
<button onClick={() => {
setLiked(!liked);
setCount(c => liked ? c - 1 : c + 1);
}}>
{liked ? 'Unlike' : 'Like'} ({count})
</button>
);
}
// app/posts/[id]/page.tsx — Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
export default async function PostPage({ params }: { params: { id: string } }) {
// Server: fetch data
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
{/* Server-rendered content (zero JS) */}
<h1>{post?.title}</h1>
<div>{post?.content}</div>
{/* Client Component island (only this ships JS) */}
<LikeButton postId={params.id} />
</article>
);
}
// Pattern: Server Component as children of Client Component
// components/Sidebar.tsx
'use client';
export function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<aside style={{ display: open ? 'block' : 'none' }}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{children} {/* Server Component content passed as children */}
</aside>
);
}Patron 3: Server Actions
Llamar funciones del servidor directamente.
// app/actions.ts — Server Action
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, content, authorId: 'user-1' },
});
revalidatePath('/posts');
}
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
// components/CreatePostForm.tsx — Client Component using Server Action
'use client';
import { createPost } from '@/app/actions';
import { useActionState } from 'react';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write your post..." required />
<button type="submit">Create Post</button>
</form>
);
}
// Works without API routes!
// The form submits directly to the server action
// Progressive enhancement: works without JavaScriptPatron 4: Streaming
La carga lenta no bloquea la pagina.
// app/dashboard/page.tsx — Streaming with Suspense
import { Suspense } from 'react';
// Slow component — fetches from external API
async function AnalyticsChart() {
const data = await fetch('https://api.analytics.com/data', {
next: { revalidate: 60 },
});
const analytics = await data.json();
return <div>Chart: {analytics.visitors} visitors</div>;
}
// Fast component — fetches from local DB
async function RecentPosts() {
const posts = await db.post.findMany({ take: 5 });
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This renders immediately */}
<Suspense fallback={<div>Loading posts...</div>}>
<RecentPosts />
</Suspense>
{/* This streams in when ready (doesn't block the page) */}
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsChart />
</Suspense>
</div>
);
}
// Result: User sees "Dashboard" + posts immediately
// Analytics chart streams in when the API responds
// No client-side loading spinners neededEstrategias de carga
Secuencial
Cuando las peticiones dependen.
// Sequential: second request depends on first
async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId); // 1st request
const posts = await getUserPosts(user.name); // 2nd (depends on user.name)
return <div>{user.name}: {posts.length} posts</div>;
}Paralelo
Promise.all para peticiones independientes.
// Parallel: independent requests — use Promise.all
async function Dashboard() {
// Start all requests simultaneously
const [users, posts, analytics] = await Promise.all([
getUsers(),
getPosts(),
getAnalytics(),
]);
return (
<div>
<UserList users={users} />
<PostList posts={posts} />
<AnalyticsPanel data={analytics} />
</div>
);
}Cache y revalidacion
Next.js extiende fetch con control de cache.
// Next.js fetch caching strategies
// 1. Static (cached indefinitely until revalidated)
const data = await fetch('https://api.example.com/data');
// Equivalent to: { cache: 'force-cache' }
// 2. Revalidate every 60 seconds (ISR)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// 3. Dynamic (no caching — fresh on every request)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// 4. Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// In a Server Action:
import { revalidateTag } from 'next/cache';
revalidateTag('posts'); // Purge all fetches tagged 'posts'Ventajas de rendimiento
RSC ofrece varias ventajas de rendimiento.
RSC Performance Benefits:
1. Zero Client JavaScript for Server Components
- Only Client Components ship JS to the browser
- Large libraries used on server (markdown, syntax highlighting) add zero bundle
2. Streaming SSR
- HTML streams to browser as components resolve
- First Contentful Paint (FCP) is faster
- Time to Interactive (TTI) is faster (less JS to hydrate)
3. Automatic Code Splitting
- Each Client Component is automatically code-split
- No manual dynamic imports needed
4. Reduced Data Transfer
- Server Components fetch data on the server (same network)
- No client-side fetch waterfalls
- No duplicate data in both HTML and JS payload
5. Smaller Hydration Payload
- Only Client Components need hydration
- Server Component output is static — no hydration needed
Typical Results:
Before RSC: 450KB JS bundle, 2.5s TTI
After RSC: 180KB JS bundle, 1.1s TTI (60% JS reduction)Errores comunes
// Mistake 1: Adding "use client" unnecessarily
// BAD: This doesn't need client-side JavaScript
'use client'; // REMOVE THIS
export function UserCard({ name, email }: { name: string; email: string }) {
return <div>{name} ({email})</div>; // No hooks, no events = Server Component
}
// Mistake 2: Importing Server Component in Client Component
// BAD: This won't work
'use client';
import { ServerComponent } from './ServerComponent'; // ERROR
// FIX: Pass as children instead
export function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
// Mistake 3: Using "use client" too high in the tree
// BAD: Making a layout a Client Component
'use client'; // This makes EVERYTHING below it client-side
export function Layout({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark');
return <div className={theme}>{children}</div>;
}
// FIX: Extract the interactive part into a small Client Component
// components/ThemeToggle.tsx
'use client';
export function ThemeToggle() {
const [theme, setTheme] = useState('dark');
return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
// layout.tsx — stays as Server Component
export function Layout({ children }: { children: React.ReactNode }) {
return <div><ThemeToggle />{children}</div>;
}Preguntas frecuentes
RSC sin Next.js?
Tecnicamente si. Practicamente Next.js es el framework principal.
RSC reemplaza SSR?
No, lo complementa.
Cuando "use client"?
Cuando necesitas hooks o APIs del navegador.
Context en Server Components?
No disponible.
Impacto SEO?
Positivo. HTML generado en el servidor.
Herramientas y guias relacionadas
- JSON Formatter - Format API responses and data
- JSON to TypeScript - Generate types from API responses
- HTML to JSX - Convert HTML to React JSX
- Next.js App Router Guide
- React Hooks Guide
- TypeScript Generics Guide
- Web Performance Optimization