Client Components y use client en Next.js | Nicolás Garzón
"use client" no significa “renderizar este componente únicamente en el navegador”. Declara una frontera de módulos: ese archivo y los módulos que importa pasan a ser candidatos para el bundle cliente y pueden usar state, Effects, eventos y APIs del navegador.
En el App Router, el árbol comienza server-first. Solo declaras cliente en los puntos que necesitan capacidades interactivas:
Texto
Copiar Server Page
├─ Server ProductDetails
├─ Server Reviews
└─ Client AddToCartButtonLa boundary ideal es tan pequeña como permita el diseño sin crear APIs artificialmente fragmentadas.
Los Server Components no pueden responder directamente a clicks ni conservar state local en el navegador. Una aplicación necesita regiones que:
Manejen formularios interactivos.
Respondan a eventos.
Utilicen useState, useReducer o useEffect.
Lean window, storage, media queries o sensores.
Integren librerías que dependen del DOM.
Proporcionen Context cliente.
"use client" marca la entrada a ese mundo.
Debe aparecer al principio del módulo, antes de imports:
TypeScript
Copiar "use client" ;
import { useState } from "react" ;
export function Counter ( ) {
const [ count, setCount] = useState ( 0 ) ;
return (
< button type= "button" onClick= { ( ) => setCount ( ( value) => value + 1 ) } >
{ count}
< / button>
) ;
} No necesita repetirse en cada archivo descendiente importado por esa boundary.
Texto
Copiar product-page.tsx server
└─ add-to-cart.tsx "use client"
├─ use-cart.ts client graph
├─ analytics.ts client graph
└─ button.tsx client graphLa directiva afecta el grafo de imports , no solo el componente exportado.
Si analytics.ts importa un SDK pesado, ese SDK puede terminar en el bundle incluso si solo se usa en una rama rara.
Durante la carga inicial, Next.js puede usar Client Components para producir HTML en servidor:
Texto
Copiar server renders initial tree
↓
HTML includes client component output
↓
browser downloads its JavaScript
↓
React hydrates itDespués de hidratación, state y eventos funcionan.
Por eso una Client Component no debe producir diferente markup inicial solo porque detecta navegador.
TypeScript
Copiar const [ open, setOpen] = useState ( false ) ; TypeScript
Copiar < button onClick= { ( ) => setOpen ( true ) } > Abrir< / button> TypeScript
Copiar useEffect ( ( ) => {
const controller = connectToBrowserService ( ) ;
return ( ) => controller. disconnect ( ) ;
} , [ ] ) ; TypeScript
Copiar navigator. clipboard. writeText ( value) ; Un hook que utiliza state o Effect debe ejecutarse desde una boundary cliente.
Este componente puede permanecer servidor:
TypeScript
Copiar export function ProductPrice ( { amount } : { amount: number } ) {
return < data value= { amount} > { formatCurrency ( amount) } < / data> ;
} No utiliza eventos ni state. Convertirlo en cliente solo aumenta el grafo.
TypeScript
Copiar
"use client" ; Toda la page y sus imports pasan al lado cliente aunque solo un filtro sea interactivo.
TypeScript
Copiar
export default async function ProductsPage ( ) {
const products = await listProducts ( ) ;
return (
< main>
< ProductsHeader / >
< ProductFilters / >
< ProductGrid products= { products} / >
< / main>
) ;
} TypeScript
Copiar
"use client" ;
export function ProductFilters ( ) {
const router = useRouter ( ) ;
} Header y grid pueden permanecer servidor.
Props que cruzan desde servidor deben ser serializables:
TypeScript
Copiar < AddToCartButton
product= { {
id: product. id,
name: product. name,
price: product. price,
} }
/ > TypeScript
Copiar < Widget database= { dbConnection} / >
< Widget onSave= { ( ) => serverSecretOperation ( ) } / >
< Widget repository= { productRepository} / > Una función normal no puede serializarse. Server Functions usan un contrato explícito diferente.
TypeScript
Copiar < DeleteButton productId= { product. id} / > TypeScript
Copiar < DeleteButton product= { entireProductRecord} / > Pasar solo el ID reduce payload y riesgo, pero la operación servidor debe volver a consultar y autorizar. No confíes en campos enviados por el cliente.
Un Server Component no puede pasar un handler normal:
TypeScript
Copiar
< ClientButton onClick= { ( ) => console . log ( "server callback" ) } / > Define el handler dentro del Client Component o pasa una Server Action admitida:
TypeScript
Copiar
< DeleteForm action= { deleteProductAction} productId= { product. id} / > El servidor todavía valida input y permiso.
Context cliente necesita provider cliente:
TypeScript
Copiar "use client" ;
import { ThemeProvider } from "next-themes" ;
export function Providers ( { children } : { children: React. ReactNode } ) {
return < ThemeProvider> { children} < / ThemeProvider> ;
} TypeScript
Copiar export default function RootLayout ( { children } : { children: React. ReactNode } ) {
return (
< html lang= "es" >
< body>
< Providers> { children} < / Providers>
< / body>
< / html>
) ;
} El provider es cliente, pero children puede contener contenido producido en servidor porque fue compuesto por el padre servidor.
Coloca providers lo más profundo posible para no ampliar actualizaciones y bundles.
Una librería puede usar Hooks sin declarar una boundary compatible en su entrypoint. Crea un wrapper:
TypeScript
Copiar "use client" ;
export { Carousel } from "third-party-carousel" ; Luego un Server Component puede importar el wrapper.
Tamaño del paquete.
Compatibilidad SSR.
Acceso a DOM durante import.
CSS necesario.
Tree shaking.
Accesibilidad.
Algunas librerías leen window al importar y fallan incluso dentro de una Client Component durante SSR.
Puedes cargar una región solo en cliente con una estrategia compatible:
TypeScript
Copiar const Map = dynamic ( ( ) => import ( "./map" ) , { ssr: false } ) ; Esto elimina su HTML inicial y puede empeorar contenido o layout. Úsalo para integraciones realmente incompatibles con servidor, no como solución general.
TypeScript
Copiar
< EditableProfile initialProfile= { profileView} / > TypeScript
Copiar "use client" ;
export function EditableProfile ( { initialProfile } : Props) {
const [ draft, setDraft] = useState ( initialProfile) ;
} initialProfile solo inicializa state. Si la prop cambia durante una navegación compatible, decide si:
Debe reiniciarse mediante key.
Debe controlarse desde arriba.
Debe combinarse con cambios locales.
Debe mantenerse el borrador.
No añadas un Effect de sincronización sin definir semántica.
Texto
Copiar server state
→ datos remotos, permisos, caché, revalidation
client state
→ input, modal, selección, estado efímeroNo copies toda respuesta servidor a una store cliente por costumbre. Conserva cliente solo lo que necesita interacción o trabajo offline.
TypeScript
Copiar export default async function ProductsPage ( {
searchParams,
} : PageProps< "/products" > ) {
const raw = await searchParams;
const filters = parseProductFilters ( raw) ;
const products = await searchProducts ( filters) ;
return (
< main>
< SearchForm initialQuery= { filters. query} / >
< ProductResults products= { products} / >
< / main>
) ;
} TypeScript
Copiar "use client" ;
export function SearchForm ( { initialQuery } : { initialQuery: string } ) {
const router = useRouter ( ) ;
const pathname = usePathname ( ) ;
const [ query, setQuery] = useState ( initialQuery) ;
function handleSubmit ( event: React. FormEvent< HTMLFormElement> ) {
event. preventDefault ( ) ;
const params = new URLSearchParams ( { query } ) ;
router. push ( ` ${ pathname} ? ${ params} ` ) ;
}
return (
< form onSubmit= { handleSubmit} >
< label htmlFor= "query" > Buscar productos< / label>
< input
id= "query"
value= { query}
onChange= { ( event) => setQuery ( event. target. value) }
/ >
< button> Buscar< / button>
< / form>
) ;
}
El servidor valida query y carga resultados.
Solo el formulario se hidrata.
El usuario modifica state local.
Submit actualiza la URL.
Next.js solicita una representación servidor nueva.
Los resultados permanecen Server Components.
Una alternativa con <form action> puede conservar progressive enhancement y reducir código cliente.
El primer render cliente debe coincidir con HTML:
TypeScript
Copiar
const [ width] = useState ( window. innerWidth) ; window no existe durante render servidor y produciría una diferencia.
Usa un valor inicial estable o useSyncExternalStore con snapshot de servidor.
No ocultes mismatches con suppressHydrationWarning sin corregir la causa.
Cada boundary añade JavaScript según su grafo. Para analizar:
Ejecuta build.
Usa bundle analyzer compatible.
Revisa imports de librerías.
Busca providers globales.
Identifica componentes que solo formatean texto.
Compara chunks por ruta.
Reducir número de Client Components no es el único objetivo; importa su peso y frecuencia.
Client Components introducen:
Descarga de JS.
Parseo y ejecución.
Hidratación.
Effects al montar.
Memoria de state.
Pero mover toda interacción a servidor puede crear roundtrips innecesarios. La decisión es un balance:
Texto
Copiar interacción inmediata y local
→ cliente
lectura de datos, secretos y contenido inicial
→ servidor"use client" vuelve inspeccionable el código y valores públicos.
Claves privadas.
SDK admin.
Módulos de DB.
Reglas que consideras secretas.
Variables privadas serializadas.
Aunque el bundler produzca error, aplica server-only y revisión.
No confíes en validación cliente. Una Action o endpoint debe validar nuevamente.
Aumenta bundle e hidratación. Extrae componentes.
Afecta su grafo de imports.
No es serializable. Define handler cliente o Server Action.
Crea loading tardío y waterfall cuando el servidor podía cargar el dato.
Añade sincronización e invalidación manual.
Sacrifica HTML inicial y puede ocultar una librería mal integrada.
Cada cambio puede actualizar consumidores amplios y obliga a cargar dependencias en todas las rutas.
Controles interactivos.
Formularios con feedback local complejo.
Estado efímero.
Event listeners.
Browser APIs.
Context cliente.
Integraciones DOM.
Formatear contenido.
Cargar datos iniciales.
Leer secretos.
Mostrar listas estáticas.
Ejecutar lógica de dominio servidor.
Envolver toda la page para usar un hook en un botón.
Prueba eventos y state con React Testing Library.
Prueba en una ruta SSR real y observa warnings.
Verifica interacción antes/después de navegación y con red lenta.
Comprueba que módulos privados no aparezcan.
Cuando usas forms/Actions, prueba sin JavaScript cuando sea requisito.
use client define una frontera de módulos.
Client Components también pueden producir HTML inicial servidor.
Todo su grafo puede entrar al bundle.
Props cruzadas deben ser serializables y mínimas.
Providers y wrappers deben colocarse profundamente.
State local no debe duplicar server state sin razón.
Cliente aporta interactividad, pero tiene coste de descarga e hidratación.
El servidor sigue autorizando y validando cada operación.
¿Por qué use client en una page puede aumentar mucho el bundle?
¿Cómo puede un Client Component mostrar un Server Component?
¿Qué diferencia existe entre un handler normal y una Server Action?
¿Por qué ssr: false no es una solución universal?
¿Qué ocurre con NEXT_PUBLIC_ dentro del grafo cliente?
¿Dónde colocarías un provider usado solo por checkout?
Ver respuestas
Porque todo el grafo importado por esa page cruza la boundary.
El padre servidor lo compone y lo pasa como children o prop ReactNode.
El handler normal vive en cliente; una Server Action utiliza el protocolo del framework y se ejecuta en servidor.
Elimina HTML inicial y puede ocultar una integración incorrecta o empeorar UX.
Su valor se inserta en el bundle durante build y es público.
En el layout o componente más cercano que cubra únicamente checkout.
Composición entre servidor y cliente convierte estas fronteras en patrones concretos para children, providers, DTOs, librerías y Server Actions.