Skip to content

Eventos ​

Los eventos permiten que tu interfaz reaccione a lo que hace el usuario: clics, teclas, envío de formularios, mover el mouse. Aprenderás a manejarlos en React, a usar el objeto evento y a comunicar un componente hijo con su padre.

Concepto ​

En React los eventos se escriben en camelCase y reciben una función (no un string):

jsx
// HTML
<button onclick="saludar()">Saludar</button>

// React
<button onClick={saludar}>Saludar</button>

Analogía: le das al botón el número de teléfono de quien debe atender la llamada (la función), no haces la llamada en ese momento.

Pasar la función, no ejecutarla ​

jsx
function App() {
  function saludar() {
    alert('¡Hola!')
  }

  return (
    <>
      {/* ✅ Pasa la referencia: se ejecuta al hacer clic */}
      <button onClick={saludar}>Bien</button>

      {/* ❌ La ejecuta al renderizar (y onClick recibe undefined) */}
      <button onClick={saludar()}>Mal</button>

      {/* ✅ Para pasar argumentos, envuelve en una función flecha */}
      <button onClick={() => alert('Hola, Ana')}>Con argumento</button>
    </>
  )
}

Eventos más comunes ​

EventoCuándo ocurreElementos
onClickClicCualquiera (sobre todo button)
onChangeCada cambio de valorinput, textarea, select
onSubmitEnvío de formularioform
onKeyDown / onKeyUpTecla presionada / soltadaElementos con foco
onFocus / onBlurRecibe / pierde el focoInputs
onMouseEnter / onMouseLeaveMouse entra / saleCualquiera
onScrollDesplazamientoElementos con scroll
onDoubleClickDoble clicCualquiera

El objeto evento ​

React pasa un objeto evento (un SyntheticEvent que funciona igual en todos los navegadores):

jsx
function Ejemplos() {
  function manejarClic(e) {
    console.log(e.type)          // "click"
    console.log(e.target)        // el elemento donde se hizo clic
    console.log(e.clientX, e.clientY)
  }

  function manejarTecla(e) {
    if (e.key === 'Enter') console.log('Enviar:', e.target.value)
    if (e.key === 'Escape') e.target.value = ''
    if (e.ctrlKey && e.key === 's') {
      e.preventDefault() // evita "Guardar página" del navegador
      console.log('Guardado')
    }
  }

  return (
    <>
      <button onClick={manejarClic}>Clic</button>
      <input onKeyDown={manejarTecla} placeholder="Presiona Enter o Escape" />
    </>
  )
}

preventDefault y stopPropagation ​

jsx
function Formulario() {
  function enviar(e) {
    e.preventDefault() // evita que el formulario recargue la página
    console.log('Enviado sin recargar')
  }

  return (
    <form onSubmit={enviar}>
      <input name="email" />
      <button type="submit">Enviar</button>
    </form>
  )
}

function Tarjeta() {
  return (
    <div onClick={() => console.log('Abrir tarjeta')} className="tarjeta">
      <h3>Producto</h3>
      <button
        onClick={(e) => {
          e.stopPropagation() // evita que el clic "suba" y abra la tarjeta
          console.log('Agregar al carrito')
        }}
      >
        Agregar
      </button>
    </div>
  )
}

Los eventos burbujean: un clic en el botón también llega a sus padres. stopPropagation corta esa cadena.

Eventos + estado ​

jsx
import { useState } from 'react'

function ContadorCaracteres() {
  const [texto, setTexto] = useState('')
  const LIMITE = 140

  return (
    <div>
      <textarea
        value={texto}
        onChange={(e) => setTexto(e.target.value.slice(0, LIMITE))}
        rows={4}
        cols={40}
      />
      <p style={{ color: texto.length > 120 ? 'crimson' : 'gray' }}>
        {texto.length}/{LIMITE}
      </p>
    </div>
  )
}

Comunicación hijo → padre ​

Los datos bajan con props. Para que un hijo avise al padre, el padre le pasa una función por props y el hijo la llama. Por convención se nombran onAlgo (prop) y manejarAlgo o handleAlgo (función).

jsx
// Componente reutilizable: no sabe qué hará el padre con el valor
export default function Calificacion({ valor, onCambiar }) {
  return (
    <div role="radiogroup" aria-label="Calificación">
      {[1, 2, 3, 4, 5].map((n) => (
        <button
          key={n}
          type="button"
          onClick={() => onCambiar(n)}
          aria-label={`${n} estrellas`}
          style={{ fontSize: 28, background: 'none', border: 'none', cursor: 'pointer' }}
        >
          {n <= valor ? '★' : '☆'}
        </button>
      ))}
    </div>
  )
}
jsx
import { useState } from 'react'
import Calificacion from './components/Calificacion'

export default function App() {
  const [estrellas, setEstrellas] = useState(0)

  const mensajes = ['', 'Muy malo', 'Malo', 'Regular', 'Bueno', '¡Excelente!']

  return (
    <main style={{ textAlign: 'center', marginTop: 40 }}>
      <h2>¿Qué te pareció el producto?</h2>
      {/* El padre guarda el estado; el hijo sólo avisa */}
      <Calificacion valor={estrellas} onCambiar={setEstrellas} />
      <p>{mensajes[estrellas]}</p>
    </main>
  )
}

Levantar el estado ​

Si dos componentes hermanos necesitan el mismo dato, el estado se mueve ("se levanta") al padre común, que lo pasa a ambos:

jsx
function App() {
  const [busqueda, setBusqueda] = useState('')
  return (
    <>
      <BarraBusqueda valor={busqueda} onCambiar={setBusqueda} />
      <ListaResultados filtro={busqueda} />
    </>
  )
}

function BarraBusqueda({ valor, onCambiar }) {
  return <input value={valor} onChange={(e) => onCambiar(e.target.value)} placeholder="Buscar..." />
}

function ListaResultados({ filtro }) {
  const frutas = ['Manzana', 'Mango', 'Pera', 'Plátano', 'Melón']
  const visibles = frutas.filter((f) => f.toLowerCase().includes(filtro.toLowerCase()))
  return <ul>{visibles.map((f) => <li key={f}>{f}</li>)}</ul>
}

Caso real: menú desplegable accesible ​

jsx
import { useState } from 'react'

export default function MenuUsuario() {
  const [abierto, setAbierto] = useState(false)

  function manejarTecla(e) {
    if (e.key === 'Escape') setAbierto(false)
  }

  return (
    <div style={{ position: 'relative', display: 'inline-block' }} onKeyDown={manejarTecla}>
      <button
        onClick={() => setAbierto((a) => !a)}
        aria-expanded={abierto}
        aria-haspopup="menu"
      >
        Mi cuenta ▾
      </button>

      {abierto && (
        <ul
          role="menu"
          style={{
            position: 'absolute', top: '110%', right: 0, listStyle: 'none', margin: 0,
            padding: 8, background: 'white', border: '1px solid #ddd', borderRadius: 8, minWidth: 160
          }}
        >
          {['Perfil', 'Pedidos', 'Cerrar sesión'].map((op) => (
            <li key={op} role="none">
              <button
                role="menuitem"
                style={{ width: '100%', textAlign: 'left', padding: 8, border: 0, background: 'none' }}
                onClick={() => {
                  alert(op)
                  setAbierto(false)
                }}
              >
                {op}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

Buena práctica

  • Usa button para acciones y a para navegar. No pongas onClick en un div para simular un botón: no es accesible con teclado.
  • Nombra las props de eventos onAlgo y los manejadores manejarAlgo/handleAlgo.

Error común

onClick={eliminar(id)} ejecuta la función en cada render (y puede borrar todo al cargar). Usa onClick={() => eliminar(id)}.

Ejercicios ​

  1. Crea un input que, al presionar Enter, agregue el texto a una lista y limpie el input. Escape debe limpiar sin agregar.
  2. Crea un componente Acordeon con preguntas frecuentes donde sólo una puede estar abierta a la vez (el estado vive en el padre).
Ver solución
jsx
// 1
import { useState } from 'react'

export function ListaRapida() {
  const [texto, setTexto] = useState('')
  const [items, setItems] = useState([])

  function manejarTecla(e) {
    if (e.key === 'Enter' && texto.trim()) {
      setItems([...items, texto.trim()])
      setTexto('')
    }
    if (e.key === 'Escape') setTexto('')
  }

  return (
    <>
      <input value={texto} onChange={(e) => setTexto(e.target.value)} onKeyDown={manejarTecla} />
      <ul>{items.map((it, i) => <li key={i}>{it}</li>)}</ul>
    </>
  )
}
jsx
// 2
import { useState } from 'react'

const preguntas = [
  { id: 1, q: '¿Hacen envíos?', a: 'Sí, a todo México.' },
  { id: 2, q: '¿Cuánto tarda?', a: 'De 3 a 5 días hábiles.' },
  { id: 3, q: '¿Puedo devolver?', a: 'Tienes 30 días.' }
]

function Item({ pregunta, abierto, onToggle }) {
  return (
    <div style={{ borderBottom: '1px solid #ddd' }}>
      <button onClick={onToggle} aria-expanded={abierto} style={{ width: '100%', textAlign: 'left', padding: 12 }}>
        {pregunta.q} {abierto ? '−' : '+'}
      </button>
      {abierto && <p style={{ padding: '0 12px' }}>{pregunta.a}</p>}
    </div>
  )
}

export function Acordeon() {
  const [abiertoId, setAbiertoId] = useState(null)
  return preguntas.map((p) => (
    <Item
      key={p.id}
      pregunta={p}
      abierto={abiertoId === p.id}
      onToggle={() => setAbiertoId(abiertoId === p.id ? null : p.id)}
    />
  ))
}

Resumen ​

  • Los eventos se escriben en camelCase y reciben una función: onClick={manejar}.
  • Para pasar argumentos usa una función flecha: onClick={() => borrar(id)}.
  • e.preventDefault() evita el comportamiento por defecto; e.stopPropagation() detiene el burbujeo.
  • Hijo → padre: el padre pasa una función onAlgo y el hijo la llama. Levanta el estado al padre común.

Academia Web — aprende, copia y construye.