Cómo usar Componentes de Servidor y Cliente

Por defecto, los diseños (layouts) y páginas son Componentes de Servidor, lo que te permite obtener datos y renderizar partes de tu interfaz de usuario en el servidor, almacenar en caché el resultado opcionalmente, y transmitirlo al cliente. Cuando necesitas interactividad o APIs del navegador, puedes usar Componentes de Cliente para añadir funcionalidad.

Esta página explica cómo funcionan los Componentes de Servidor y Cliente en Next.js y cuándo usarlos, con ejemplos de cómo combinarlos en tu aplicación.

¿Cuándo usar Componentes de Servidor y Cliente?

Los entornos del cliente y servidor tienen capacidades diferentes. Los Componentes de Servidor y Cliente te permiten ejecutar lógica en cada entorno según tu caso de uso.

Usa Componentes de Cliente cuando necesites:

Usa Componentes de Servidor cuando necesites:

  • Obtener datos de bases de datos o APIs cercanas a la fuente.
  • Usar claves de API, tokens y otros secretos sin exponerlos al cliente.
  • Reducir la cantidad de JavaScript enviado al navegador.
  • Mejorar el First Contentful Paint (FCP), y transmitir contenido progresivamente al cliente.

Por ejemplo, el componente <Page> es un Componente de Servidor que obtiene datos sobre una publicación, y los pasa como props a <LikeButton> que maneja la interactividad del lado del cliente.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}

¿Cómo funcionan los Componentes de Servidor y Cliente en Next.js?

En el servidor

En el servidor, Next.js usa las APIs de React para orquestar el renderizado. El trabajo de renderizado se divide en fragmentos, por segmentos de ruta individuales (diseños y páginas):

  • Componentes de Servidor se renderizan en un formato especial llamado React Server Component Payload (RSC Payload).
  • Componentes de Cliente y el RSC Payload se usan para prerenderizar HTML.

¿Qué es el React Server Component Payload (RSC)?

El RSC Payload es una representación binaria compacta del árbol de Componentes de Servidor renderizados. React lo usa en el cliente para actualizar el DOM del navegador. El RSC Payload contiene:

  • El resultado renderizado de los Componentes de Servidor
  • Marcadores de posición para dónde deben renderizarse los Componentes de Cliente y referencias a sus archivos JavaScript
  • Cualquier prop pasado de un Componente de Servidor a un Componente de Cliente

En el cliente (primera carga)

Luego, en el cliente:

  1. HTML se usa para mostrar inmediatamente una vista previa rápida y no interactiva de la ruta al usuario.
  2. RSC Payload se usa para reconciliar los árboles de Componentes de Cliente y Servidor.
  3. JavaScript se usa para hidratar los Componentes de Cliente y hacer la aplicación interactiva.

¿Qué es la hidratación?

La hidratación es el proceso de React para adjuntar manejadores de eventos al DOM, para hacer el HTML estático interactivo.

En navegaciones posteriores:

  • El RSC Payload se pre-carga y almacena en caché para una navegación instantánea.
  • Componentes de Cliente se renderizan completamente en el cliente, sin el HTML renderizado en el servidor.

Ejemplos

Usando Componentes de Cliente

Puedes crear un Componente de Cliente añadiendo la directiva "use client" al inicio del archivo, antes de tus imports.

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

"use client" se usa para declarar un límite entre los grafos (árboles) de módulos de Servidor y Cliente.

Una vez que un archivo está marcado con "use client", todos sus imports y componentes hijos se consideran parte del paquete del cliente. Esto significa que no necesitas añadir la directiva a cada componente destinado al cliente.

Reduciendo el tamaño del paquete JS

Para reducir el tamaño de tus paquetes de JavaScript del cliente, añade 'use client' a componentes interactivos específicos en lugar de marcar grandes partes de tu UI como Componentes de Cliente.

Por ejemplo, el componente <Layout> contiene principalmente elementos estáticos como un logo y enlaces de navegación, pero incluye una barra de búsqueda interactiva. <Search /> es interactivo y necesita ser un Componente de Cliente, sin embargo, el resto del diseño puede permanecer como un Componente de Servidor.

'use client'

export default function Search() {
  // ...
}

Pasando datos de Componentes de Servidor a Cliente

Puedes pasar datos de Componentes de Servidor a Componentes de Cliente usando props.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return <LikeButton likes={post.likes} />
}

Alternativamente, puedes transmitir datos de un Componente de Servidor a un Componente de Cliente con el Hook use. Consulta un ejemplo.

Nota importante: Las props pasadas a Componentes de Cliente deben ser serializables por React.

Intercalando Componentes de Servidor y Cliente

Puedes pasar Componentes de Servidor como prop a un Componente de Cliente. Esto te permite anidar visualmente UI renderizada en el servidor dentro de componentes del cliente.

Un patrón común es usar children para crear un slot en un <ClientComponent>. Por ejemplo, un componente <Cart> que obtiene datos en el servidor, dentro de un componente <Modal> que usa estado del cliente para alternar la visibilidad.

'use client'

export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

Luego, en un Componente de Servidor padre (ej. <Page>), puedes pasar un <Cart> como hijo del <Modal>:

import Modal from './ui/modal'
import Cart from './ui/cart'

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

En este patrón, todos los Componentes de Servidor se renderizarán en el servidor con anticipación, incluyendo aquellos pasados como props. El RSC Payload resultante contendrá referencias de dónde deben renderizarse los Componentes de Cliente dentro del árbol de componentes.

Proveedores de contexto

El contexto de React se usa comúnmente para compartir estado global como el tema actual. Sin embargo, el contexto de React no es compatible con Componentes de Servidor.

Para usar contexto, crea un Componente de Cliente que acepte children:

'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

Luego, impórtalo en un Componente de Servidor (ej. layout):

import ThemeProvider from './theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Tu Componente de Servidor ahora podrá renderizar directamente tu proveedor, y todos los demás Componentes de Cliente en tu aplicación podrán consumir este contexto.

Nota importante: Debes renderizar los proveedores lo más profundo posible en el árbol – observa cómo ThemeProvider solo envuelve {children} en lugar de todo el documento <html>. Esto facilita que Next.js optimice las partes estáticas de tus Componentes de Servidor.

Componentes de terceros

Cuando uses un componente de terceros que dependa de características exclusivas del cliente, puedes envolverlo en un Componente de Cliente para asegurarte de que funcione como se espera.

Por ejemplo, el <Carousel /> puede importarse del paquete acme-carousel. Este componente usa useState, pero aún no tiene la directiva "use client".

Si usas <Carousel /> dentro de un Componente de Cliente, funcionará como se espera:

'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Ver imágenes</button>
      {/* Funciona, ya que Carousel se usa dentro de un Componente de Cliente */}
      {isOpen && <Carousel />}
    </div>
  )
}

Sin embargo, si intentas usarlo directamente dentro de un Componente de Servidor, verás un error. Esto se debe a que Next.js no sabe que <Carousel /> usa características exclusivas del cliente.

Para solucionarlo, puedes envolver componentes de terceros que dependan de características exclusivas del cliente en tus propios Componentes de Cliente:

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

Ahora, puedes usar <Carousel /> directamente dentro de un Componente de Servidor:

import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>Ver imágenes</p>
      {/* Funciona, ya que Carousel es un Componente de Cliente */}
      <Carousel />
    </div>
  )
}

Consejo para autores de bibliotecas

Si estás construyendo una biblioteca de componentes, añade la directiva "use client" a los puntos de entrada que dependan de características exclusivas del cliente. Esto permite que tus usuarios importen componentes en Componentes de Servidor sin necesidad de crear wrappers.

Vale la pena mencionar que algunos empaquetadores podrían eliminar las directivas "use client". Puedes encontrar un ejemplo de cómo configurar esbuild para incluir la directiva "use client" en los repositorios React Wrap Balancer y Vercel Analytics.

Prevención de envenenamiento del entorno (environment poisoning)

Los módulos de JavaScript pueden compartirse entre módulos de Componentes del Servidor (Server Components) y del Cliente (Client Components). Esto significa que es posible importar accidentalmente código exclusivo del servidor en el cliente. Por ejemplo, considere la siguiente función:

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Esta función contiene una API_KEY que nunca debería exponerse al cliente.

En Next.js, solo las variables de entorno con el prefijo NEXT_PUBLIC_ se incluyen en el paquete del cliente. Si las variables no tienen este prefijo, Next.js las reemplaza con una cadena vacía.

Como resultado, aunque getData() puede importarse y ejecutarse en el cliente, no funcionará como se espera.

Para prevenir el uso accidental en Componentes del Cliente (Client Components), puede utilizar el paquete server-only.

Terminal
npm install server-only

Luego, importe el paquete en un archivo que contenga código exclusivo del servidor:

lib/data.js
import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Ahora, si intenta importar este módulo en un Componente del Cliente (Client Component), se producirá un error en tiempo de compilación.

Nota importante: El paquete correspondiente client-only puede utilizarse para marcar módulos que contienen lógica exclusiva del cliente, como código que accede al objeto window.