Server Components vs Client Components en React y NextJS: Guia Practica
Aprende las diferencias entre Server Components y Client Components en React y NextJS. Cuando usar cada uno, patrones de composicion, errores comunes y como decidir.
Server Components vs Client Components en React y NextJS
La distincion entre Server Components y Client Components en React define donde se ejecuta tu codigo, que APIs tienes disponibles, y cuanto JavaScript termina en el navegador del usuario. Desde que NextJS adopto App Router como estandar, entender esta diferencia paso de ser opcional a ser requisito para cualquier proyecto serio.
Este articulo cubre que son, cuando usar cada uno, como combinarlos, y los errores que vas a cometer (y como evitarlos).
Que son los Server Components
Los Server Components (RSC) son componentes de React que se ejecutan exclusivamente en el servidor. El navegador nunca recibe el codigo JavaScript de estos componentes -- solo recibe el HTML resultante.
Esto tiene consecuencias directas:
- No envias JavaScript al cliente por ese componente
- Puedes acceder directamente a bases de datos, filesystem, y APIs internas
- No puedes usar hooks de estado (
useState), efectos (useEffect), ni event handlers (onClick)
En NextJS con App Router, todos los componentes son Server Components por defecto. No necesitas hacer nada especial para que un componente sea de servidor.
Ejemplo basico de Server Component
// app/blog/page.tsx
// Este es un Server Component por defecto -- no hay "use client"
async function obtenerPosts() {
const res = await fetch('https://api.ejemplo.com/posts', {
next: { revalidate: 3600 }
})
return res.json()
}
export default async function BlogPage() {
const posts = await obtenerPosts()
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map((post: { id: string; titulo: string; resumen: string }) => (
<li key={post.id}>
<h2>{post.titulo}</h2>
<p>{post.resumen}</p>
</li>
))}
</ul>
</main>
)
}Observa algo clave: el componente es async. Los Server Components pueden ser funciones asincronas directamente. No necesitas useEffect ni estados de carga manuales para obtener datos -- simplemente haces await dentro del componente.
Async components
Solo los Server Components pueden ser async. Si pones async en un Client Component, React lanzara un error. Para data fetching en Client Components, necesitas usar useEffect o librerias como SWR/TanStack Query.
Que puedes hacer en un Server Component
- Consultar bases de datos directamente (Prisma, Drizzle, SQL raw)
- Leer el filesystem con
fs - Usar variables de entorno del servidor (sin prefijo
NEXT_PUBLIC_) - Hacer fetch a APIs internas sin exponer URLs ni tokens
- Importar librerias pesadas sin afectar el bundle del cliente
// app/dashboard/page.tsx
import { db } from '@/lib/database'
import { formatearFecha } from '@/lib/utils'
export default async function Dashboard() {
// Consulta directa a la base de datos
const usuarios = await db.usuario.findMany({
where: { activo: true },
orderBy: { creadoEn: 'desc' },
take: 10,
})
// Acceso a variable de entorno del servidor
const apiInterna = process.env.INTERNAL_API_URL
return (
<section>
<h1>Dashboard</h1>
<p>Conectado a: {apiInterna}</p>
<ul>
{usuarios.map((u) => (
<li key={u.id}>
{u.nombre} -- {formatearFecha(u.creadoEn)}
</li>
))}
</ul>
</section>
)
}Que NO puedes hacer en un Server Component
Esto es igual de importante. Si intentas cualquiera de estas cosas en un Server Component, vas a obtener un error:
// ESTO FALLA EN UN SERVER COMPONENT
export default function ServerComp() {
// Error: useState no funciona en Server Components
const [count, setCount] = useState(0)
// Error: useEffect no funciona en Server Components
useEffect(() => {
console.log('montado')
}, [])
// Error: event handlers no funcionan en Server Components
return <button onClick={() => alert('click')}>Click</button>
}Si ya entiendes como funciona el ciclo de vida de React con hooks como useEffect, sabes que estos dependen del navegador para ejecutarse. Si necesitas repasar esos conceptos, la guia del ciclo de vida de React lo cubre en detalle.
Que son los Client Components
Los Client Components son componentes que se ejecutan en el navegador del usuario. Tienen acceso al DOM, a las Web APIs, y a todo el sistema de estado y efectos de React.
Para declarar un Client Component, agregas la directiva "use client" al inicio del archivo:
'use client'
import { useState } from 'react'
export function Contador() {
const [count, setCount] = useState(0)
return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setCount(count + 1)}>
Incrementar
</button>
</div>
)
}La directiva "use client" marca un limite (boundary). Todo lo que se importa desde ese archivo tambien se considera parte del bundle del cliente.
'use client' marca un limite
Cuando pones "use client" en un archivo, ese archivo y todos sus imports pasan a ser parte del bundle del cliente. Si importas una libreria pesada en un Client Component, esa libreria va al navegador del usuario.
Que puedes hacer en un Client Component
- Usar hooks:
useState,useEffect,useReducer,useRef,useContext - Manejar eventos:
onClick,onChange,onSubmit,onKeyDown - Acceder a Web APIs:
window,document,localStorage,navigator - Usar librerias que dependen del navegador (animaciones, graficas, editores)
'use client'
import { useState, useEffect, useRef } from 'react'
export function BuscadorInteractivo() {
const [query, setQuery] = useState('')
const [resultados, setResultados] = useState<string[]>([])
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// Focus automatico al montar
inputRef.current?.focus()
}, [])
useEffect(() => {
if (query.length < 2) {
setResultados([])
return
}
const timeout = setTimeout(async () => {
const res = await fetch(`/api/buscar?q=${encodeURIComponent(query)}`)
const data = await res.json()
setResultados(data.resultados)
}, 300)
return () => clearTimeout(timeout)
}, [query])
return (
<div>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
<ul>
{resultados.map((r, i) => (
<li key={i}>{r}</li>
))}
</ul>
</div>
)
}Que NO puedes hacer en un Client Component
- Usar
async/awaitdirectamente en el componente (no puede serasync function) - Acceder al filesystem del servidor
- Consultar bases de datos directamente
- Usar variables de entorno sin prefijo
NEXT_PUBLIC_
'use client'
// ESTO FALLA
export default async function ClientComp() {
// Error: Client Components no pueden ser async
const data = await fetch('/api/datos')
return <div>{data}</div>
}Para obtener datos en Client Components, usas useEffect o librerias de data fetching. Si quieres profundizar en como hacer peticiones HTTP correctamente, revisa la guia de Fetch API.
Tabla comparativa
Esta tabla resume las diferencias clave entre ambos tipos:
| Caracteristica | Server Component | Client Component |
|---|---|---|
| Directiva | Ninguna (default) | "use client" |
| Donde se ejecuta | Solo servidor | Servidor (SSR) + navegador |
useState / useReducer | No | Si |
useEffect / useLayoutEffect | No | Si |
Event handlers (onClick) | No | Si |
Web APIs (window, document) | No | Si |
async/await en componente | Si | No |
| Acceso a DB / filesystem | Si | No |
| Variables env del servidor | Si | No |
| JavaScript enviado al cliente | No | Si |
| Puede importar Server Components | Si | No (solo como children) |
| Puede importar Client Components | Si | Si |
Regla general
Si tu componente no necesita interactividad ni hooks, dejalo como Server Component. El codigo no se envia al navegador y la pagina carga mas rapido.
Patrones de composicion
Combinar Server Components y Client Components es donde esta el poder real del modelo. Hay patrones claros que funcionan y anti-patrones que te van a romper el build.
Patron 1: Server Component como padre, Client Component como hijo
Este es el patron mas comun. El servidor obtiene los datos y los pasa al cliente para la interactividad:
// app/productos/page.tsx (Server Component)
import { db } from '@/lib/database'
import { ListaProductos } from '@/components/ListaProductos'
export default async function ProductosPage() {
const productos = await db.producto.findMany({
where: { publicado: true },
select: { id: true, nombre: true, precio: true, imagen: true },
})
// Pasa datos serializables al Client Component
return (
<main>
<h1>Productos</h1>
<ListaProductos productos={productos} />
</main>
)
}// components/ListaProductos.tsx (Client Component)
'use client'
import { useState } from 'react'
interface Producto {
id: string
nombre: string
precio: number
imagen: string
}
export function ListaProductos({ productos }: { productos: Producto[] }) {
const [filtro, setFiltro] = useState('')
const productosFiltrados = productos.filter((p) =>
p.nombre.toLowerCase().includes(filtro.toLowerCase())
)
return (
<div>
<input
type="text"
value={filtro}
onChange={(e) => setFiltro(e.target.value)}
placeholder="Filtrar productos..."
/>
<div className="grid grid-cols-3 gap-4">
{productosFiltrados.map((p) => (
<div key={p.id} className="border p-4 rounded">
<img src={p.imagen} alt={p.nombre} />
<h3>{p.nombre}</h3>
<p>${p.precio}</p>
</div>
))}
</div>
</div>
)
}El Server Component obtiene los productos de la base de datos. El Client Component recibe esos datos como props y maneja el filtrado interactivo. El usuario nunca ve la consulta a la DB ni el token de acceso.
Patron 2: Client Component que envuelve Server Components via children
Este patron es esencial cuando necesitas un layout interactivo que contenga contenido del servidor:
// components/Sidebar.tsx (Client Component)
'use client'
import { useState, type ReactNode } from 'react'
export function Sidebar({ children }: { children: ReactNode }) {
const [abierto, setAbierto] = useState(true)
return (
<div className="flex">
<aside className={abierto ? 'w-64' : 'w-0'}>
<button onClick={() => setAbierto(!abierto)}>
{abierto ? 'Cerrar' : 'Abrir'}
</button>
{abierto && <nav>{children}</nav>}
</aside>
</div>
)
}// app/docs/layout.tsx (Server Component)
import { Sidebar } from '@/components/Sidebar'
import { db } from '@/lib/database'
export default async function DocsLayout({
children,
}: {
children: React.ReactNode
}) {
const categorias = await db.categoria.findMany({
include: { paginas: true },
})
return (
<div className="flex">
<Sidebar>
{/* Este contenido es Server Component, pasado como children */}
<ul>
{categorias.map((cat) => (
<li key={cat.id}>
<strong>{cat.nombre}</strong>
<ul>
{cat.paginas.map((p) => (
<li key={p.id}>
<a href={`/docs/${p.slug}`}>{p.titulo}</a>
</li>
))}
</ul>
</li>
))}
</ul>
</Sidebar>
<main>{children}</main>
</div>
)
}El truco es que children ya esta renderizado como RSC antes de llegar al Client Component. El Sidebar solo controla si se muestra o no -- no necesita saber que el contenido viene del servidor.
Patron 3: Separar la logica interactiva en componentes pequenos
En lugar de hacer todo un componente Client, extrae solo la parte interactiva:
MAL: Todo como Client Component
'use client'
import { useState, useEffect } from 'react'
export default function ArticuloPage() {
const [articulo, setArticulo] = useState(null)
const [likes, setLikes] = useState(0)
useEffect(() => {
fetch('/api/articulo/1')
.then(r => r.json())
.then(setArticulo)
}, [])
if (!articulo) return <p>Cargando...</p>
return (
<article>
<h1>{articulo.titulo}</h1>
<p>{articulo.contenido}</p>
<button onClick={() => setLikes(l => l + 1)}>
{likes} likes
</button>
</article>
)
}BIEN: Solo lo interactivo como Client
// app/articulo/[id]/page.tsx (Server Component)
import { db } from '@/lib/database'
import { BotonLike } from '@/components/BotonLike'
export default async function ArticuloPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const articulo = await db.articulo.findUnique({
where: { id },
})
return (
<article>
<h1>{articulo.titulo}</h1>
<p>{articulo.contenido}</p>
<BotonLike articuloId={id} />
</article>
)
}
// components/BotonLike.tsx (Client Component)
'use client'
import { useState } from 'react'
export function BotonLike({ articuloId }: { articuloId: string }) {
const [likes, setLikes] = useState(0)
async function handleLike() {
setLikes(l => l + 1)
await fetch(`/api/like/${articuloId}`, {
method: 'POST',
})
}
return (
<button onClick={handleLike}>
{likes} likes
</button>
)
}La version de la derecha envia mucho menos JavaScript al navegador. El contenido del articulo se renderiza en el servidor y llega como HTML puro. Solo el boton de likes necesita JavaScript.
Patron 4: Provider de contexto como Client, contenido como Server
Si necesitas un contexto de React (tema, autenticacion, etc.), el Provider debe ser Client Component, pero su contenido puede ser Server:
// components/ThemeProvider.tsx (Client Component)
'use client'
import { createContext, useContext, useState, type ReactNode } from 'react'
const ThemeContext = createContext<{
tema: string
cambiarTema: () => void
}>({
tema: 'dark',
cambiarTema: () => {},
})
export function ThemeProvider({ children }: { children: ReactNode }) {
const [tema, setTema] = useState('dark')
const cambiarTema = () => {
setTema(tema === 'dark' ? 'light' : 'dark')
}
return (
<ThemeContext.Provider value={{ tema, cambiarTema }}>
<div data-theme={tema}>{children}</div>
</ThemeContext.Provider>
)
}
export function useTheme() {
return useContext(ThemeContext)
}// app/layout.tsx (Server Component)
import { ThemeProvider } from '@/components/ThemeProvider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<body>
<ThemeProvider>
{/* Todo lo de adentro puede ser Server Components */}
{children}
</ThemeProvider>
</body>
</html>
)
}El ThemeProvider es Client, pero children sigue renderizandose como Server Components. El Provider solo agrega la funcionalidad de cambio de tema sin forzar todo el arbol a ser cliente.
Errores comunes y como evitarlos
Error 1: Usar hooks en un Server Component
// app/page.tsx (Server Component por defecto)
// Error: useState y useEffect solo funcionan en Client Components
import { useState, useEffect } from 'react'
export default function Home() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData)
}, [])
return <div>{data}</div>
}El error:
Error: useState only works in Client Components. Add the "use client" directive
at the top of the file to use it.Solucion: Si necesitas hooks, agrega "use client". Si solo necesitas obtener datos, usa async/await directamente:
// SOLUCION A: Hacerlo Server Component con async
export default async function Home() {
const res = await fetch('https://api.ejemplo.com/data')
const data = await res.json()
return <div>{JSON.stringify(data)}</div>
}
// SOLUCION B: Agregar "use client" si necesitas interactividad
// Ponlo al inicio del archivo y usa useEffectError 2: Importar un Server Component en un Client Component
'use client'
// Esto causa un error
import { ServerDataComponent } from './ServerDataComponent'
export function ClientWrapper() {
return (
<div>
<ServerDataComponent />
</div>
)
}Por que falla: Cuando un archivo tiene "use client", todo lo que importa se convierte en parte del bundle del cliente. Un Server Component que usa fs, db, o APIs del servidor no puede ejecutarse en el navegador.
Solucion: Usa el patron de children:
// components/ClientWrapper.tsx
'use client'
import { type ReactNode } from 'react'
export function ClientWrapper({ children }: { children: ReactNode }) {
const [visible, setVisible] = useState(true)
return (
<div>
<button onClick={() => setVisible(!visible)}>Toggle</button>
{visible && children}
</div>
)
}// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper'
import { ServerDataComponent } from '@/components/ServerDataComponent'
export default function Page() {
return (
<ClientWrapper>
<ServerDataComponent />
</ClientWrapper>
)
}Error 3: Pasar datos no serializables como props
// app/page.tsx (Server Component)
import { ClientComp } from '@/components/ClientComp'
export default async function Page() {
const data = await getData()
return (
<ClientComp
// Error: Las funciones no son serializables
onSave={async () => {
await db.save(data)
}}
// Error: Date no es serializable directamente
fecha={new Date()}
// Error: Map no es serializable
cache={new Map()}
/>
)
}Solucion: Solo pasa datos serializables (strings, numeros, booleanos, arrays, objetos planos). Convierte los tipos no serializables:
export default async function Page() {
const data = await getData()
return (
<ClientComp
// Pasa la fecha como string ISO
fecha={new Date().toISOString()}
// Pasa el Map como objeto plano
cache={Object.fromEntries(miMap)}
// Para acciones del servidor, usa Server Actions
guardarAction={guardarDatos}
/>
)
}
// Server Action definida aparte
async function guardarDatos(formData: FormData) {
'use server'
await db.save(Object.fromEntries(formData))
}Error 4: Hacer todo Client Component "por si acaso"
Es tentador poner "use client" en todos lados para evitar errores. Pero esto anula las ventajas del modelo:
- Bundle mas grande: Todo el JavaScript se envia al navegador
- Sin acceso directo a datos: Necesitas APIs intermedias para todo
- Mas latencia: El navegador tiene que descargar, parsear y ejecutar mas JS
- Sin streaming: Los Server Components soportan streaming nativo con Suspense
Regla practica: Empieza sin "use client". Agregalo solo cuando el compilador te lo pida o cuando necesites interactividad.
Error 5: Olvidar que "use client" afecta a todos los imports
'use client'
// Esta libreria de 200KB ahora va al bundle del cliente
import { generarReporte } from '@/lib/reportes-pesados'
export function BotonReporte() {
return (
<button onClick={() => generarReporte()}>
Generar Reporte
</button>
)
}Si reportes-pesados usa dependencias grandes (como una libreria de PDFs), todo eso va al navegador.
Solucion: Usa dynamic import para cargar bajo demanda:
'use client'
export function BotonReporte() {
async function handleClick() {
const { generarReporte } = await import('@/lib/reportes-pesados')
generarReporte()
}
return (
<button onClick={handleClick}>
Generar Reporte
</button>
)
}Arbol de decision: como elegir
Usa este flujo para decidir si tu componente debe ser Server o Client:
Tu componente necesita...
1. useState, useReducer o useContext?
--> SI: Client Component
2. useEffect o useLayoutEffect?
--> SI: Client Component
3. Event handlers (onClick, onChange, onSubmit)?
--> SI: Client Component
4. APIs del navegador (window, document, localStorage)?
--> SI: Client Component
5. Librerias que dependen del navegador (framer-motion, chart.js)?
--> SI: Client Component
6. Ninguna de las anteriores?
--> Server Component (dejalo como esta)Cuando tengas duda
Preguntate: "Este componente necesita hacer algo DESPUES de que la pagina cargo?" Si la respuesta es si, probablemente necesitas un Client Component. Si solo muestra datos, dejalo como Server Component.
Casos especificos
Formularios: Depende. Si es un formulario simple que usa Server Actions, puede ser Server Component. Si necesitas validacion en tiempo real, autocompletado o estados de carga complejos, Client Component.
// Formulario simple con Server Action (Server Component)
export default function ContactoPage() {
async function enviar(formData: FormData) {
'use server'
const nombre = formData.get('nombre')
const email = formData.get('email')
await db.contacto.create({ data: { nombre, email } })
}
return (
<form action={enviar}>
<input name="nombre" required />
<input name="email" type="email" required />
<button type="submit">Enviar</button>
</form>
)
}// Formulario complejo con validacion en tiempo real (Client Component)
'use client'
import { useState } from 'react'
export function FormularioRegistro() {
const [nombre, setNombre] = useState('')
const [errores, setErrores] = useState<Record<string, string>>({})
function validarNombre(valor: string) {
if (valor.length < 3) {
setErrores({ nombre: 'Minimo 3 caracteres' })
} else {
setErrores({})
}
setNombre(valor)
}
return (
<form>
<input
value={nombre}
onChange={(e) => validarNombre(e.target.value)}
/>
{errores.nombre && <span className="text-red-500">{errores.nombre}</span>}
</form>
)
}Tablas con sorting/filtrado: Client Component para la interactividad, pero carga los datos desde un Server Component padre.
Componentes de navegacion: Si solo muestran links, Server Component. Si necesitan saber la ruta activa o manejar estado del menu, Client Component.
Modales y dropdowns: Client Component, porque necesitan estado para abrirse/cerrarse.
Rendimiento: por que importa
La diferencia no es solo de funcionalidad. Tiene un impacto directo en el rendimiento:
Bundle size
Un Server Component que importa una libreria de 500KB (como un parser de Markdown) no agrega ni un byte al bundle del cliente. El mismo componente marcado como Client enviaria esos 500KB al navegador.
// Server Component: 0 bytes al cliente
import { marked } from 'marked' // 500KB de libreria
export default async function BlogPost({ slug }: { slug: string }) {
const markdown = await fs.readFile(`./posts/${slug}.md`, 'utf-8')
const html = marked(markdown)
return <article dangerouslySetInnerHTML={{ __html: html }} />
}Streaming y Suspense
Los Server Components soportan streaming nativo. Esto significa que React puede enviar partes de la pagina al navegador mientras otras todavia se estan renderizando:
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* Esto aparece inmediatamente */}
<Suspense fallback={<p>Cargando metricas...</p>}>
<Metricas />
</Suspense>
{/* Esto puede tardar, pero no bloquea lo de arriba */}
<Suspense fallback={<p>Cargando actividad reciente...</p>}>
<ActividadReciente />
</Suspense>
</main>
)
}
async function Metricas() {
const data = await fetch('https://api.ejemplo.com/metricas')
const metricas = await data.json()
return (
<div className="grid grid-cols-4 gap-4">
{metricas.map((m: { label: string; valor: number }) => (
<div key={m.label} className="p-4 border rounded">
<p className="text-sm">{m.label}</p>
<p className="text-2xl font-bold">{m.valor}</p>
</div>
))}
</div>
)
}
async function ActividadReciente() {
const data = await fetch('https://api.ejemplo.com/actividad')
const actividad = await data.json()
return (
<ul>
{actividad.map((a: { id: string; descripcion: string }) => (
<li key={a.id}>{a.descripcion}</li>
))}
</ul>
)
}El usuario ve el titulo y el skeleton de carga inmediatamente. Cada seccion se llena cuando sus datos estan listos, sin bloquear el resto.
Time to Interactive (TTI)
Con Server Components, el HTML llega listo para ser leido. No hay que esperar a que JavaScript se descargue, se parsee y se ejecute antes de ver contenido. Esto mejora significativamente metricas como LCP (Largest Contentful Paint) y TTI.
Estructura recomendada de un proyecto
Una estructura que aprovecha bien Server y Client Components separa claramente la logica interactiva:
app/
layout.tsx (Server - estructura base)
page.tsx (Server - homepage)
blog/
page.tsx (Server - lista de posts)
[slug]/
page.tsx (Server - post individual)
dashboard/
page.tsx (Server - carga datos)
layout.tsx (Server - layout del dashboard)
components/
ui/
Button.tsx (Client - interactivo)
Modal.tsx (Client - estado abierto/cerrado)
Input.tsx (Client - controlled input)
Card.tsx (Server - solo presentacion)
Badge.tsx (Server - solo presentacion)
blog/
PostCard.tsx (Server - muestra datos)
ComentarioForm.tsx (Client - formulario interactivo)
BotonCompartir.tsx (Client - onClick)
layout/
Header.tsx (Client - menu responsive)
Footer.tsx (Server - contenido estatico)
Sidebar.tsx (Client - toggle abierto/cerrado)
providers/
ThemeProvider.tsx (Client - contexto)
AuthProvider.tsx (Client - contexto)
La regla es clara: si no necesita interactividad, es Server. Solo marcas como Client lo que realmente lo requiere.
Resumen de patrones
| Patron | Server Component | Client Component |
|---|---|---|
| Obtener datos | Directamente con async/await | Con useEffect o SWR |
| Formulario simple | Server Action | useState + onChange |
| Lista estatica | Renderizar directamente | No necesario |
| Lista con filtrado | Pasa datos como props | Maneja el filtro con estado |
| Modal/Dropdown | No aplica | useState para abrir/cerrar |
| Contexto/Provider | No aplica | Crear Provider, pasar children |
| Componente de layout | Default | Solo si tiene toggle/estado |
| SEO metadata | generateMetadata | No aplica |
Preguntas frecuentes
Los Server Components reemplazan a getServerSideProps?
Si, efectivamente. En el App Router de NextJS, ya no necesitas getServerSideProps ni getStaticProps. Los Server Components hacen fetch directamente dentro del componente. El resultado es el mismo (datos del servidor) pero con una API mucho mas simple.
Se puede usar useContext en Server Components?
No. useContext es un hook y solo funciona en Client Components. Si necesitas compartir datos entre Server Components, pasalos como props o usa un sistema de cache como React.cache().
Los Client Components se renderizan solo en el cliente?
No. Aunque se llaman "Client Components", Next.js los pre-renderiza en el servidor como HTML (SSR) y luego los hidrata en el cliente. El nombre se refiere a que tambien se ejecutan en el cliente, no a que solo se ejecutan ahi. La diferencia con Server Components es que estos nunca llegan al cliente como JavaScript.
Puedo convertir un proyecto existente de Pages Router a Server Components?
Si, pero es un proceso gradual. Requiere migrar de Pages Router a App Router. No puedes usar Server Components con Pages Router. La documentacion de NextJS sobre migracion detalla los pasos.
Que librerias no funcionan con Server Components?
Cualquier libreria que use hooks (useState, useEffect), acceda al DOM, o use APIs del navegador. Esto incluye librerias de animacion (framer-motion), librerias de formularios (react-hook-form), librerias de graficas (recharts, chart.js), y cualquier libreria de UI que maneje estado interno. Siempre puedes envolver esas librerias en un Client Component y pasarle datos desde el servidor.
Recursos adicionales
- React Server Components -- documentacion oficial de React sobre RSC
- NextJS Rendering: Server and Client Components -- guia oficial de NextJS sobre rendering
- NextJS Composition Patterns -- patrones de composicion recomendados por el equipo de NextJS
Preguntas frecuentes
Cual es la diferencia principal entre Server Components y Client Components?
Server Components se ejecutan unicamente en el servidor y nunca envian JavaScript al navegador, lo que reduce el bundle size. Client Components se ejecutan en el navegador y tienen acceso a APIs del DOM, estado con useState y efectos con useEffect. La diferencia clave es donde se ejecuta el codigo y que APIs tienes disponibles.
Puedo usar hooks como useState y useEffect en Server Components?
No. Los hooks de estado y efectos (useState, useEffect, useReducer, etc.) solo funcionan en Client Components porque requieren el ciclo de vida del navegador. Si necesitas interactividad, debes usar la directiva 'use client' al inicio del archivo.
Como paso datos de un Server Component a un Client Component?
Pasas datos como props. El Server Component obtiene los datos (de base de datos, API, filesystem) y los pasa al Client Component como props serializables. Los datos deben ser serializables: strings, numeros, booleanos, arrays y objetos planos. No puedes pasar funciones, clases o Dates directamente.
Que pasa si no pongo 'use client' en mi componente?
En NextJS con App Router, todos los componentes son Server Components por defecto. Si no pones 'use client', el componente se ejecuta solo en el servidor. Esto significa que no puedes usar hooks de estado, event handlers ni APIs del navegador en ese componente.
Un Client Component puede importar un Server Component?
No directamente. Un Client Component no puede importar un Server Component con import. Sin embargo, puedes pasar un Server Component como children o como prop de tipo ReactNode a un Client Component. Este patron de composicion es fundamental para combinar ambos tipos.
Articulos relacionados
Next.js 16: Guia de Migracion y Novedades
Migra tu proyecto de Next.js 15 a 16. Novedades principales, breaking changes, y pasos para actualizar sin romper tu app.
Testing en Next.js con Vitest y Playwright
Configura testing en tu proyecto Next.js. Unit tests con Vitest, E2E con Playwright, y como integrarlos en tu pipeline de CI/CD.
Tailwind CSS 4: Migracion desde v3
Migra tu proyecto de Tailwind CSS 3 a 4. Cambios principales, nuevo sistema de configuracion, CSS-first config y como actualizar sin romper tu app.