Componentes del Cliente
Agrega interactividad con "use client" -- hooks, manejadores de eventos y APIs del navegador.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// app/components/counter.tsx
"use client";
import { useState } from "react";
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount((c) => c + 1)}>
Conteo: {count}
</button>
);
}// app/page.tsx (Server Component importa el Client Component)
import { Counter } from "./components/counter";
export default function Page() {
return (
<main>
<h1>Bienvenido</h1>
<Counter initialCount={5} />
</main>
);
}Cuándo usarlo: Necesitas useState, useEffect, useRef, manejadores de eventos (onClick, onChange), o APIs del navegador (window, localStorage, IntersectionObserver).
Ejemplo Funcional
// app/components/search-autocomplete.tsx
"use client";
import { useState, useEffect, useRef, useTransition } from "react";
import { useRouter } from "next/navigation";
type Suggestion = { id: string; label: string };
export function SearchAutocomplete() {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const inputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
useEffect(() => {
if (query.length < 2) {
setSuggestions([]);
return;
}
const controller = new AbortController();
fetch(`/api/suggestions?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((res) => res.json())
.then((data) => setSuggestions(data))
.catch(() => {}); // ignorar errores de abort
return () => controller.abort();
}, [query]);
function handleSelect(suggestion: Suggestion) {
setQuery(suggestion.label);
setIsOpen(false);
startTransition(() => {
router.push(`/search?q=${encodeURIComponent(suggestion.label)}`);
});
}
return (
<div className="relative w-full max-w-md">
<input
ref={inputRef}
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value);
setIsOpen(true);
}}
onFocus={() => setIsOpen(true)}
placeholder="Buscar..."
className="w-full border rounded px-4 py-2"
/>
{isOpen && suggestions.length > 0 && (
<ul className="absolute top-full left-0 right-0 bg-white border rounded-b shadow-lg z-10">
{suggestions.map((s) => (
<li key={s.id}>
<button
onClick={() => handleSelect(s)}
className="w-full text-left px-4 py-2 hover:bg-gray-100"
>
{s.label}
</button>
</li>
))}
</ul>
)}
{isPending && (
<span className="absolute right-3 top-2.5 text-sm text-gray-400">
Cargando...
</span>
)}
</div>
);
}// app/search/page.tsx (Server Component)
import { SearchAutocomplete } from "@/app/components/search-autocomplete";
export default function SearchPage() {
return (
<main className="p-6">
<h1 className="text-2xl font-bold mb-4">Búsqueda</h1>
<SearchAutocomplete />
</main>
);
}Lo que esto demuestra:
- La directiva
"use client"en la parte superior del archivo - Múltiples hooks:
useState,useEffect,useRef,useTransition - Manejadores de eventos:
onChange,onFocus,onClick - Obtención de datos en el cliente con limpieza a través de
AbortController - Usar
useRouterpara navegación programática - Un Server Component importando y renderizando el Client Component
Análisis Profundo
Cómo Funciona
- Agregar
"use client"al inicio de un archivo lo marca como un límite de cliente. El componente y todos sus importes se incluyen en el bundle JavaScript del cliente. - Los Client Components se renderizan en el servidor en la carga de página inicial (SSR). El HTML se genera en el servidor, se envía al navegador y luego se hidrata -- React adjunta manejadores de eventos y hace el componente interactivo.
- Durante la navegación del lado del cliente, los Client Components se renderizan completamente en el cliente usando la carga útil de RSC del servidor.
- La directiva
"use client"se aplica al archivo, no a un único componente. Todos los exportes de ese archivo se convierten en Client Components. - Los Client Components pueden importar otros Client Components (con o sin
"use client"en el hijo). No pueden importar Server Components directamente. - Los props pasados de un Server Component a un Client Component deben ser serializables.
Variaciones
Entrada de formulario controlada:
"use client";
import { useState } from "react";
export function EmailForm({ onSubmitAction }: { onSubmitAction: (email: string) => Promise<void> }) {
const [email, setEmail] = useState("");
return (
<form action={async () => { await onSubmitAction(email); }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Suscribirse</button>
</form>
);
}Uso seguro de APIs del navegador:
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <p>Ventana: {size.width} x {size.height}</p>;
}Envolvimiento de una librería de cliente de terceros:
// app/components/map.tsx
"use client";
import { MapContainer, TileLayer, Marker } from "react-leaflet";
export function Map({ lat, lng }: { lat: number; lng: number }) {
return (
<MapContainer center={[lat, lng]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker position={[lat, lng]} />
</MapContainer>
);
}Notas de TypeScript
// Los props del servidor deben ser serializables
type ClientProps = {
initialData: string[]; // OK
count: number; // OK
serverAction: (id: string) => Promise<void>; // OK (Server Action)
// onClick: () => void; // NOT OK (función regular)
// ref: React.Ref<T>; // NOT OK (no serializable)
};
// Tipos de manejador de eventos
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { ... }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { ... }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }Errores Comunes
-
"use client"no significa "solo cliente" -- Los Client Components se renderizan en el servidor (SSR) en la solicitud inicial. Se ejecutan en el servidor y el cliente. Solución: Si necesitas renderización verdaderamente solo del cliente, usadynamic(() => import("..."), { ssr: false }). -
Desincronización de hidratación -- Si el HTML renderizado en el servidor difiere del renderizado del cliente (por ejemplo, usando
Date.now(),Math.random()o verificaciones dewindow), React registra un error de hidratación. Solución: UsauseEffectpara valores que difieren entre servidor y cliente, osuppressHydrationWarningpara desincronizaciones intencionales. -
Importar un Server Component en un Client Component -- Esto no está permitido. El import se tratará como un Client Component. Solución: Pasa el Server Component como
childrenu otro prop de JSX en su lugar. -
Aumento del tamaño del bundle -- Todo lo que se importa en un archivo
"use client"termina en el bundle del cliente, incluidas las librerías de utilidades. Solución: Mantén los archivos"use client"pequeños y enfocados. Importa librerías pesadas solo donde sea necesario. -
Todos los exportes se convierten en client components -- Si exportas tanto una función de utilidad como un componente desde un archivo
"use client", la utilidad también se vuelve solo del cliente. Solución: Mantén las utilidades en archivos separados, no cliente. -
Los hooks no pueden ser condicionales -- Los hooks de React deben ser llamados en el mismo orden en cada renderización. Solución: Nunca pongas hooks dentro de bloques
if, bucles o retornos anticipados.
Alternativas
| Enfoque | Usar Cuando | No Usar Cuando |
|---|---|---|
| Client Components | UI interactivo con state, efectos o APIs del navegador | Visualización pura de datos sin interactividad |
| Server Components | Renderización de solo lectura, obtención de datos, sin JS enviado | Necesitas hooks o manejadores de eventos |
dynamic(import, { ssr: false }) | El componente nunca debe renderizarse en el servidor (por ejemplo, librerías de canvas) | SSR está bien y solo necesitas hidratación |
| Web Components | Necesitas elementos interactivos independientes del framework | Los componentes React funcionan para tu caso de uso |
Server Actions (form action) | Puedes manejar la interacción con un envío de formulario | Necesitas retroalimentación del lado del cliente en tiempo real |
Preguntas Frecuentes
¿Qué hace realmente la directiva "use client"?
- Marca el archivo como un límite de cliente -- el componente y todos sus importes se incluyen en el bundle JavaScript del cliente.
- Se aplica a todo el archivo, no a un único componente.
- Los Client Components se renderizan en el servidor (SSR) para el HTML inicial, luego se hidratan en el cliente.
¿Los Client Components se renderizan solo en el navegador?
No. Los Client Components se renderizan en el servidor en la carga de página inicial (SSR). El servidor envía HTML, luego React lo hidrata en el cliente para adjuntar manejadores de eventos e hacerlo interactivo.
¿Cuándo debo usar un Client Component en lugar de un Server Component?
Usa un Client Component cuando necesites:
- Hooks de React (
useState,useEffect,useRef,useTransition) - Manejadores de eventos (
onClick,onChange,onFocus) - APIs del navegador (
window,localStorage,IntersectionObserver)
¿Cómo accedo de forma segura a APIs del navegador como window o localStorage?
Envuelve el uso de la API del navegador en useEffect para que solo se ejecute en el cliente después de la hidratación:
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <p>Ancho: {width}</p>;
}¿Puede un Client Component importar un Server Component?
No. Importar un Server Component dentro de un archivo "use client" lo convierte silenciosamente en un Client Component. En su lugar, pasa el Server Component como children u otro prop de JSX desde un padre Server Component.
¿Qué props se pueden pasar de un Server Component a un Client Component?
- Los props deben ser serializables: strings, números, booleanos, arrays, objetos planos y Server Actions.
- Las funciones regulares, instancias de clase, Refs y Symbols no son serializables y fallarán.
Error: ¿Por qué agregar "use client" a un archivo aumenta significativamente el tamaño de mi bundle?
Porque "use client" extrae todos los importes en ese archivo al bundle del cliente, incluidas las librerías de utilidades. Soluciona esto manteniendo los archivos "use client" pequeños y enfocados -- extrae la lógica pesada en archivos separados que no sean cliente.
Error: Veo un error de desincronización de hidratación -- ¿qué lo causa?
Los errores de hidratación ocurren cuando el HTML renderizado en el servidor difiere del renderizado del cliente. Las causas comunes son:
- Usar
Date.now()oMath.random()durante el renderizado - Leer propiedades de
windowdurante el renderizado en lugar de enuseEffect - Solución: Mueve valores que difieren en
useEffect, o usasuppressHydrationWarningpara desincronizaciones intencionales.
¿Cómo hago que un componente se renderice solo en el cliente sin SSR?
Usa next/dynamic con ssr: false:
import dynamic from "next/dynamic";
const ClientOnlyMap = dynamic(
() => import("./map"),
{ ssr: false }
);¿Cómo escribo tipos para manejadores de eventos en un Client Component con TypeScript?
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { }¿Cómo escribo tipos para props de un Client Component que recibe una Server Action?
Las Server Actions se escriben como funciones asincrónicas que devuelven una Promise:
type FormProps = {
onSubmitAction: (email: string) => Promise<void>;
};Las funciones regulares (no Server Actions) no son válidas como props serializables.
¿Cuál es el patrón AbortController mostrado en el ejemplo de autocompletar búsqueda?
- El
AbortControllercancela solicitudes fetch en vuelo cuando se ejecuta la limpieza deuseEffect(por ejemplo, cuandoquerycambia). - Esto evita que respuestas obsoletas sobrescriban resultados más nuevos.
- El
.catch(() => {})ignora el error de abort.
¿Qué hace useTransition en un Client Component?
useTransitionte permite marcar una actualización de state como no urgente para que no bloquee la UI.isPendingestruemientras la transición se está ejecutando, lo cual es útil para mostrar indicadores de carga.- En el ejemplo, envuelve
router.push()para evitar bloquear la entrada mientras se navega.
Relacionado
- Server Components -- El modo de renderización predeterminado
- Composition Patterns -- Mezclar componentes de servidor y cliente
- Search Params --
useSearchParamsen Client Components - Server Actions -- Pasar acciones como props a Client Components