Client Components
Los Client Components son componentes de React que se ejecutan en el navegador. Te permiten agregar interactividad, usar hooks y responder a eventos del usuario.
¿Qué son los Client Components?
Los Client Components se renderizan en el servidor primero (para SEO), pero su JavaScript se envía al navegador para hacerlos interactivos.
Permiten:
- ✅ Usar hooks de React (
useState
,useEffect
, etc.) - ✅ Manejar eventos (
onClick
,onChange
, etc.) - ✅ Usar browser APIs (
window
,localStorage
, etc.) - ✅ Usar librerías que dependen del navegador
No por defecto
A diferencia de Server Components, los Client Components requieren la directiva 'use client'
al inicio del archivo.
Tu primer Client Component
Para crear un Client Component, agrega 'use client'
al inicio del archivo:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Incrementar
</button>
</div>
)
}
La directiva 'use client'
Debe ser la primera línea del archivo, antes de cualquier import.
Cuándo usar Client Components
Usa Client Components cuando necesites:
1. Interactividad con el usuario
'use client'
import { useState } from 'react'
export default function SearchBar() {
const [query, setQuery] = useState('')
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
console.log('Buscando:', query)
}
return (
<form onSubmit={handleSearch}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
<button type="submit">Buscar</button>
</form>
)
}
2. Estado local
'use client'
import { useState } from 'react'
export default function ToggleButton() {
const [isActive, setIsActive] = useState(false)
return (
<button
onClick={() => setIsActive(!isActive)}
className={isActive ? 'active' : 'inactive'}
>
{isActive ? 'Activo' : 'Inactivo'}
</button>
)
}
3. Efectos y subscripciones
'use client'
import { useState, useEffect } from 'react'
export default function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>Segundos: {seconds}</div>
}
4. Browser APIs
'use client'
import { useEffect, useState } from 'react'
export default function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
handleResize() // Initial size
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return <div>Ventana: {size.width} x {size.height}</div>
}
5. Context API
'use client'
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
return useContext(ThemeContext)
}
Límites de Client Components
Los Client Components SÍ agregan JavaScript al bundle del cliente:
'use client'
import { useState } from 'react'
import heavyLibrary from 'heavy-lib' // ⚠️ Esto se enviará al cliente
export default function Component() {
const [data, setData] = useState(null)
const process = () => {
const result = heavyLibrary.process(data) // Se ejecuta en el navegador
setData(result)
}
return <button onClick={process}>Procesar</button>
}
Cuida el tamaño del bundle
Cada librería que importes en un Client Component se enviará al navegador. Úsalos estratégicamente.
Patrones comunes
Pattern 1: Botones y forms interactivos
'use client'
import { useState } from 'react'
export default function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
if (!res.ok) {
setError('Credenciales inválidas')
}
} catch (err) {
setError('Error de conexión')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
{error && <p className="error">{error}</p>}
<button type="submit">Login</button>
</form>
)
}
Pattern 2: Modals y overlays
'use client'
import { useState } from 'react'
export default function Modal({ trigger, children }) {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>
{trigger}
</button>
{isOpen && (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={() => setIsOpen(false)}>Cerrar</button>
</div>
</div>
)}
</>
)
}
Pattern 3: Tabs y accordions
'use client'
import { useState } from 'react'
export default function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0)
return (
<div>
<div className="tab-buttons">
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className={activeTab === index ? 'active' : ''}
>
{tab.label}
</button>
))}
</div>
<div className="tab-content">
{tabs[activeTab].content}
</div>
</div>
)
}
Pattern 4: Auto-save
'use client'
import { useState, useEffect } from 'react'
export default function AutoSaveInput() {
const [value, setValue] = useState('')
const [saved, setSaved] = useState(true)
useEffect(() => {
// Auto-save después de 1 segundo sin cambios
const timeout = setTimeout(async () => {
if (!saved) {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ value }),
})
setSaved(true)
}
}, 1000)
return () => clearTimeout(timeout)
}, [value, saved])
const handleChange = (e) => {
setValue(e.target.value)
setSaved(false)
}
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
/>
<span>{saved ? '✓ Guardado' : 'Guardando...'}</span>
</div>
)
}
Hooks comunes en Client Components
useState - Estado local
'use client'
import { useState } from 'react'
export default function Example() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')
const [isOpen, setIsOpen] = useState(false)
// ...
}
useEffect - Efectos secundarios
'use client'
import { useEffect } from 'react'
export default function Example() {
useEffect(() => {
// Se ejecuta después del render
console.log('Component mounted')
return () => {
// Cleanup
console.log('Component unmounted')
}
}, []) // Dependencies array
}
useRef - Referencias a elementos
'use client'
import { useRef, useEffect } from 'react'
export default function Example() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// Focus automático
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}
Custom hooks
'use client'
import { useState, useEffect } from 'react'
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
export default function SearchComponent() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
useEffect(() => {
if (debouncedQuery) {
// Hacer búsqueda con el valor debounced
console.log('Searching:', debouncedQuery)
}
}, [debouncedQuery])
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)
}
Optimización de Client Components
1. Keep them small
Extrae solo la parte interactiva como Client Component:
// ✅ BUENO: Solo el botón es client
// app/page.tsx (Server Component)
import InteractiveButton from '@/components/InteractiveButton'
export default function Page() {
return (
<div>
<h1>Mi Página</h1>
<p>Mucho contenido estático...</p>
<InteractiveButton /> {/* Solo esto es client */}
</div>
)
}
// components/InteractiveButton.tsx (Client Component)
'use client'
import { useState } from 'react'
export default function InteractiveButton() {
const [clicked, setClicked] = useState(false)
return <button onClick={() => setClicked(true)}>Click me</button>
}
2. Code splitting
NextJS hace code splitting automáticamente por cada Client Component:
// Estos se cargan como bundles separados
import Modal from '@/components/Modal' // Bundle 1
import Carousel from '@/components/Carousel' // Bundle 2
import Chart from '@/components/Chart' // Bundle 3
3. Lazy loading
Para componentes pesados que no se necesitan inicialmente:
'use client'
import { lazy, Suspense } from 'react'
const HeavyChart = lazy(() => import('@/components/HeavyChart'))
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Cargando gráfico...</div>}>
<HeavyChart />
</Suspense>
</div>
)
}
Próximos pasos
Aprende a combinar Server y Client Components eficientemente:
- Composición - Server + Client patterns
- Data Fetching - Obtener datos en ambos tipos
- Server Actions - Mutations desde el cliente
Resumen
- Client Components requieren
'use client'
al inicio del archivo - Permiten interactividad, hooks y browser APIs
- Agregan JavaScript al bundle del cliente
- Mantén Client Components pequeños y específicos
- Combínalos estratégicamente con Server Components