Componentes del Cliente
Agrega interactividad con "use client" -- hooks, manejadores de eventos y APIs del navegador.
Busca en todas las páginas de la documentación
Agrega interactividad con "use client" -- hooks, manejadores de eventos y APIs del navegador.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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).
// 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:
"use client" en la parte superior del archivouseState, useEffect, useRef, useTransitiononChange, onFocus, onClickAbortControlleruseRouter para navegación programática"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."use client" se aplica al archivo, no a un único componente. Todos los exportes de ese archivo se convierten en Client Components."use client" en el hijo). No pueden importar Server Components directamente.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>
);
}// 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>) { ... }"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, usa dynamic(() => 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 de window), React registra un error de hidratación. Solución: Usa useEffect para valores que difieren entre servidor y cliente, o suppressHydrationWarning para 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 children u 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.
| 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 |
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.
Usa un Client Component cuando necesites:
useState, useEffect, useRef, useTransition)onClick, onChange, onFocus)window, localStorage, IntersectionObserver)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>;
}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.
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.
Los errores de hidratación ocurren cuando el HTML renderizado en el servidor difiere del renderizado del cliente. Las causas comunes son:
Date.now() o Math.random() durante el renderizadowindow durante el renderizado en lugar de en useEffectuseEffect, o usa suppressHydrationWarning para desincronizaciones intencionales.Usa next/dynamic con ssr: false:
import dynamic from "next/dynamic";
const ClientOnlyMap = dynamic(
() => import("./map"),
{ ssr: false }
);function handleClick(e: React.MouseEvent<HTMLButtonElement>) { }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { }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.
AbortController cancela solicitudes fetch en vuelo cuando se ejecuta la limpieza de useEffect (por ejemplo, cuando query cambia)..catch(() => {}) ignora el error de abort.useTransition te permite marcar una actualización de state como no urgente para que no bloquee la UI.isPending es true mientras la transición se está ejecutando, lo cual es útil para mostrar indicadores de carga.router.push() para evitar bloquear la entrada mientras se navega.useSearchParams en Client ComponentsRevisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥