Client Components
Client Components se ejecutan en el navegador. Los necesitas cuando tu UI requiere interactividad, estado local o APIs del browser.
Cuando usar Client Components
| Necesitas | Componente |
|---|---|
| Obtener datos del servidor | Server |
| Acceder a la DB directamente | Server |
| onClick, onChange, onSubmit | Client |
| useState, useEffect, useRef | Client |
| localStorage, cookies del browser | Client |
| Librerias que usan el DOM (charts, maps, editors) | Client |
Crear un Client Component
Agrega "use client" en la primera linea del archivo:
tsx
"use client"
import { useState, useEffect } from "react"
export default function SearchBar() {
const [query, setQuery] = useState("")
const [results, setResults] = useState([])
useEffect(() => {
if (query.length < 3) return
const timer = setTimeout(async () => {
const res = await fetch(`/api/search?q=${query}`)
const data = await res.json()
setResults(data)
}, 300)
return () => clearTimeout(timer)
}, [query])
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
className="border rounded px-3 py-2 w-full"
/>
{results.length > 0 && (
<ul className="mt-2 border rounded divide-y">
{results.map((r: { id: string; title: string }) => (
<li key={r.id} className="px-3 py-2">{r.title}</li>
))}
</ul>
)}
</div>
)
}
Patrones comunes
Formulario interactivo en un layout servidor
tsx
// app/contacto/page.tsx — Server Component
import ContactForm from "./ContactForm"
export default function ContactoPage() {
return (
<div className="max-w-lg mx-auto py-12">
<h1 className="text-3xl font-bold mb-6">Contacto</h1>
<p className="text-gray-400 mb-8">
Mandanos un mensaje y te respondemos en 24 horas.
</p>
{/* Client Component para el formulario */}
<ContactForm />
</div>
)
}
tsx
// app/contacto/ContactForm.tsx
"use client"
import { useState } from "react"
export default function ContactForm() {
const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle")
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setStatus("sending")
const formData = new FormData(e.currentTarget)
await fetch("/api/contacto", {
method: "POST",
body: formData,
})
setStatus("sent")
}
if (status === "sent") {
return <p className="text-green-400">Mensaje enviado. Gracias.</p>
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="nombre"
placeholder="Tu nombre"
required
className="w-full border rounded px-3 py-2"
/>
<input
name="email"
type="email"
placeholder="tu@email.com"
required
className="w-full border rounded px-3 py-2"
/>
<textarea
name="mensaje"
placeholder="Tu mensaje"
rows={4}
required
className="w-full border rounded px-3 py-2"
/>
<button
type="submit"
disabled={status === "sending"}
className="bg-blue-600 text-white px-6 py-2 rounded disabled:opacity-50"
>
{status === "sending" ? "Enviando..." : "Enviar"}
</button>
</form>
)
}
Componente con libreria del DOM
tsx
// components/MapView.tsx
"use client"
import { useEffect, useRef } from "react"
export default function MapView({ lat, lng }: { lat: number; lng: number }) {
const mapRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// Inicializar mapa (Leaflet, Mapbox, etc.)
if (!mapRef.current) return
// La libreria necesita acceso al DOM
}, [lat, lng])
return <div ref={mapRef} className="h-96 w-full rounded" />
}
Hook personalizado
tsx
// hooks/useLocalStorage.ts
"use client"
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(initialValue)
useEffect(() => {
const stored = localStorage.getItem(key)
if (stored) {
setValue(JSON.parse(stored))
}
}, [key])
function updateValue(newValue: T) {
setValue(newValue)
localStorage.setItem(key, JSON.stringify(newValue))
}
return [value, updateValue] as const
}
Limites de "use client"
Cuando marcas un archivo con "use client", ese componente y todos sus imports se vuelven Client Components. Por eso:
- Manten los Client Components lo mas pequenos posible
- No pongas
"use client"en archivos que no lo necesitan - Usa el patron de composicion: Server Component padre con Client Component hijo
tsx
// MAL: todo el page es Client Component
"use client"
import { useState } from "react"
export default function Page() {
const [open, setOpen] = useState(false)
// ... 200 lineas de codigo que no necesitan estado
}
// BIEN: solo el boton es Client Component
// page.tsx (Server Component)
import ToggleButton from "./ToggleButton"
export default function Page() {
return (
<div>
{/* 200 lineas de contenido estatico */}
<ToggleButton />
</div>
)
}