guias·15 min de lectura

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

tsx
// 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
tsx
// 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:

tsx
// 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:

tsx
'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)
tsx
'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/await directamente en el componente (no puede ser async function)
  • Acceder al filesystem del servidor
  • Consultar bases de datos directamente
  • Usar variables de entorno sin prefijo NEXT_PUBLIC_
tsx
'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:

CaracteristicaServer ComponentClient Component
DirectivaNinguna (default)"use client"
Donde se ejecutaSolo servidorServidor (SSR) + navegador
useState / useReducerNoSi
useEffect / useLayoutEffectNoSi
Event handlers (onClick)NoSi
Web APIs (window, document)NoSi
async/await en componenteSiNo
Acceso a DB / filesystemSiNo
Variables env del servidorSiNo
JavaScript enviado al clienteNoSi
Puede importar Server ComponentsSiNo (solo como children)
Puede importar Client ComponentsSiSi
💡
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:

tsx
// 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>
  )
}
tsx
// 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:

tsx
// 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>
  )
}
tsx
// 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

tsx
'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

tsx
// 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:

tsx
// 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)
}
tsx
// 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

tsx
// 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:

plaintext
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:

tsx
// 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 useEffect

Error 2: Importar un Server Component en un Client Component

tsx
'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:

tsx
// 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>
  )
}
tsx
// 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

tsx
// 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:

tsx
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

tsx
'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:

tsx
'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:

plaintext
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.

tsx
// 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>
  )
}
tsx
// 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.

tsx
// 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:

tsx
// 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:

Estructura de archivos

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

PatronServer ComponentClient Component
Obtener datosDirectamente con async/awaitCon useEffect o SWR
Formulario simpleServer ActionuseState + onChange
Lista estaticaRenderizar directamenteNo necesario
Lista con filtradoPasa datos como propsManeja el filtro con estado
Modal/DropdownNo aplicauseState para abrir/cerrar
Contexto/ProviderNo aplicaCrear Provider, pasar children
Componente de layoutDefaultSolo si tiene toggle/estado
SEO metadatagenerateMetadataNo 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#nextjs#server-components#client-components#rendering

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.