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 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:

  1. Composición - Server + Client patterns
  2. Data Fetching - Obtener datos en ambos tipos
  3. 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