Client Components en Next.js

Aprende qué son los Client Components, cuándo usarlos, cómo se renderizan y cómo combinarlos con Server Components en Next.js App Router.

Client Components en Next.js

Serie: Entendiendo Next.js — Este artículo es parte de la serie "Entendiendo Next.js", donde exploramos cómo funciona Next.js internamente.

¿Qué son los Client Components?

Los Client Components son componentes que se ejecutan en el navegador y permiten agregar interactividad a tu aplicación. Son los componentes que conocés de React puro: pueden usar estado, responder a eventos y acceder a APIs del navegador.

En Next.js con App Router, los componentes son Server Components por defecto. Para convertir un componente en Client Component, simplemente agregás la directiva 'use client' al principio del archivo.

tsx
// components/Counter.tsx
'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)}>Me gusta</button>
    </div>
  )
}

¿Cuándo usar un Client Component?

Usá Client Components cuando necesites:

  • Estado e interactividad: useState, useReducer, event handlers como onClick o onChange
  • Efectos y ciclo de vida: useEffect, useLayoutEffect
  • APIs del navegador: localStorage, window, navigator.geolocation, etc.
  • Custom hooks que dependan de cualquiera de los puntos anteriores

Si tu componente no necesita nada de esto, lo más probable es que pueda ser un Server Component.

El límite de 'use client'

La directiva 'use client' no es solo una etiqueta para un componente, sino que declara un límite entre el grafo de módulos del servidor y el del cliente.

Una vez que marcás un archivo con 'use client', todos sus imports y componentes hijos pasan automáticamente a ser parte del bundle del cliente. No hace falta agregar la directiva en cada archivo hijo.

tsx
// components/SearchBar.tsx
'use client'

import { useState } from 'react'
import SearchSuggestions from './SearchSuggestions' // también va al cliente

export default function SearchBar() {
  const [query, setQuery] = useState('')

  return (
    <div>
      <input onChange={e => setQuery(e.target.value)} />
      <SearchSuggestions query={query} />
    </div>
  )
}

Por eso, la buena práctica es agregar 'use client' lo más abajo posible en el árbol de componentes. Así el resto de la UI sigue siendo un Server Component y el JavaScript que se envía al navegador se mantiene al mínimo.

tsx
// app/layout.tsx — Server Component
import Search from './search'  // Client Component
import Logo from './logo'      // Server Component

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />  {/* Solo este componente y sus hijos van al cliente */}
      </nav>
      <main>{children}</main>
    </>
  )
}

¿Cómo se renderizan los Client Components?

En la primera carga, Next.js genera el HTML inicial del árbol de componentes en el servidor, incluyendo el markup de los Client Components. El usuario ve contenido de inmediato, antes de que cargue JavaScript.

El proceso es:

  • Next.js renderiza el árbol de Server Components en el servidor
  • Los Client Components se representan como placeholders en el RSC Payload
  • El navegador recibe el HTML completo y lo muestra
  • Se descarga el JavaScript de los Client Components
  • React hidrata esos componentes, adjuntando los event listeners

En navegaciones posteriores dentro de la app, Next.js solicita un nuevo RSC Payload al servidor y React actualiza solo las partes necesarias de la interfaz.

Pasar datos del servidor al cliente

La forma correcta de compartir datos entre un Server Component y un Client Component es a través de props. El Server Component obtiene los datos y los pasa hacia abajo.

tsx
// app/posts/[id]/page.tsx — Server Component
import LikeButton from '@/components/LikeButton'
import { getPost } from '@/lib/data'

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

  return (
    <div>
      <h1>{post.title}</h1>
      <LikeButton likes={post.likes} />
    </div>
  )
}
tsx
// components/LikeButton.tsx — Client Component
'use client'

import { useState } from 'react'

export default function LikeButton({ likes }: { likes: number }) {
  const [count, setCount] = useState(likes)

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

Un detalle importante: los props que se pasan a Client Components deben ser serializables. Esto significa que no podés pasar funciones, clases, o cualquier valor que no pueda convertirse a JSON. Strings, números, arrays, objetos planos y booleans están bien.

Combinar Server y Client Components

Podés pasar Server Components como children de un Client Component. Esto es útil cuando necesitás un wrapper interactivo que contenga contenido estático del servidor.

Un ejemplo típico es un modal: el modal en sí maneja estado para abrirse y cerrarse (Client Component), pero el contenido adentro puede ser un Server Component que trae datos.

tsx
// components/Modal.tsx — Client Component
'use client'

import { useState } from 'react'

export default function Modal({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Abrir</button>
      {isOpen && <div>{children}</div>}
    </>
  )
}
tsx
// app/page.tsx — Server Component
import Modal from '@/components/Modal'
import Cart from '@/components/Cart'  // Server Component

export default function Page() {
  return (
    <Modal>
      <Cart />  {/* Se renderiza en el servidor, se pasa como prop */}
    </Modal>
  )
}

En este patrón, Cart se renderiza en el servidor antes de llegar al cliente. El RSC Payload incluye una referencia a dónde debe insertarse dentro del árbol del Client Component.

Context en Client Components

React Context no está disponible en Server Components. Si necesitás compartir estado global como el tema de la aplicación, creás un Client Component que actúa como provider y lo envolvés en el layout.

tsx
// app/theme-provider.tsx
'use client'

import { createContext, useContext } from 'react'

const ThemeContext = createContext<'light' | 'dark'>('light')

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

export function useTheme() {
  return useContext(ThemeContext)
}
tsx
// app/layout.tsx — Server Component
import { ThemeProvider } from './theme-provider'

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

Una buena práctica es envolver el provider lo más profundo posible en el árbol, no en el <html> completo. Así Next.js puede optimizar mejor las partes estáticas de la app.

Librerías de terceros

Muchas librerías de npm usan useState, useEffect u otras APIs del cliente pero no incluyen la directiva 'use client'. Si intentás importar una de estas directamente en un Server Component, vas a ver un error.

La solución es crear un wrapper que marque esa librería como Client Component:

tsx
// components/Carousel.tsx
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel
tsx
// app/page.tsx — Server Component
import Carousel from '@/components/Carousel'

export default function Page() {
  return <Carousel />  // Funciona porque el wrapper es Client Component
}

Proteger código solo del cliente: client-only

Así como existe server-only para proteger código del servidor, existe el paquete client-only para marcar módulos que solo deben ejecutarse en el navegador, como los que acceden a window o localStorage.

bash
npm install client-only
ts
// lib/analytics.ts
import 'client-only'

export function trackEvent(name: string) {
  window.gtag('event', name)
}

Si alguien intenta importar este módulo en un Server Component, Next.js lanzará un error en build time.

Coste de los Client Components

Cada Client Component agrega JavaScript que el navegador debe descargar, parsear y ejecutar. Ese coste se acumula:

  • Bundle size: más código JavaScript que viaja al navegador
  • Parse time: el navegador necesita parsear ese JS antes de ejecutarlo
  • Hydration cost: React debe recorrer el árbol y adjuntar los event listeners

Por eso Next.js recomienda mantener la interactividad lo más aislada posible. Un componente que solo muestra datos no debería ser un Client Component aunque esté rodeado de interactividad.

Cuándo NO usar Client Components

Evitá convertir componentes en Client Components si:

  • Solo muestran datos estáticos o hacen fetch
  • No necesitan estado ni interactividad
  • No usan APIs del navegador

Cada Client Component agrega JavaScript al bundle que el usuario tiene que descargar. Cuantos menos Client Components tengas, más rápida va a ser tu app.

Emanuel López

Emanuel López

Desarrollador de Software · Montevideo, Uruguay

Mantente al día

Te aviso cuando publique algo nuevo. Podés darte de baja cuando quieras.