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

NecesitasComponente
Obtener datos del servidorServer
Acceder a la DB directamenteServer
onClick, onChange, onSubmitClient
useState, useEffect, useRefClient
localStorage, cookies del browserClient
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>
  )
}