Qué es un evento en React y cómo se diferencia del DOM
En React, los eventos son la forma estándar de responder a la interacción del usuario (clics, escritura, envío de formularios). Se declaran como props en elementos JSX usando nombres en camelCase, por ejemplo onClick, onChange y onSubmit. En lugar de pasar una cadena como en HTML, se pasa una función (un handler) que React ejecutará cuando ocurra el evento.
Idea clave: el handler debe describir “qué hacer” cuando ocurre el evento, y normalmente actualizará el estado mediante su setter. React se encarga de volver a renderizar con el nuevo estado.
onClick: responder a clics sin ejecutar en render
Patrón básico
function Counter() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button type="button" onClick={handleClick}>
Incrementar ({count})
</button>
);
}Observa que onClick={handleClick} pasa la función, no la ejecuta. Si escribes onClick={handleClick()}, se ejecutará durante el render y no cuando el usuario haga clic.
Actualizar estado basado en el valor previo (forma segura)
Cuando el nuevo estado depende del anterior, usa la forma funcional del setter. Esto evita errores por actualizaciones agrupadas (batching) o múltiples cambios seguidos.
function Counter() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
return (
<button type="button" onClick={handleClick}>
+2 (actual: {count})
</button>
);
}Con la forma funcional, cada actualización recibe el valor más reciente, incluso si React agrupa renders.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
onChange: capturar inputs de forma controlada
Un input “controlado” es aquel cuyo valor proviene del estado, y se actualiza con onChange. Así, la UI y el estado siempre están sincronizados.
Input de texto controlado
function NameField() {
const [name, setName] = React.useState("");
function handleChange(e) {
setName(e.target.value);
}
return (
<div>
<label htmlFor="name">Nombre</label>
<input
id="name"
type="text"
value={name}
onChange={handleChange}
autoComplete="name"
/>
<p>Hola, {name || "(sin nombre)"}</p>
</div>
);
}Guía práctica paso a paso: convertir un input a controlado
- 1) Crea estado para el valor:
const [value, setValue] = useState(""). - 2) Asigna
value={value}al<input>. - 3) Añade
onChangey dentro usasetValue(e.target.value). - 4) Evita mezclar
defaultValueconvalueen el mismo input (controlado vs. no controlado).
Checkbox y select controlados
function Preferences() {
const [accepted, setAccepted] = React.useState(false);
const [country, setCountry] = React.useState("ES");
return (
<form>
<div>
<input
id="terms"
type="checkbox"
checked={accepted}
onChange={e => setAccepted(e.target.checked)}
/>
<label htmlFor="terms">Acepto los términos</label>
</div>
<div>
<label htmlFor="country">País</label>
<select
id="country"
value={country}
onChange={e => setCountry(e.target.value)}
>
<option value="ES">España</option>
<option value="MX">México</option>
<option value="AR">Argentina</option>
</select>
</div>
</form>
);
}Regla rápida: texto usa e.target.value, checkbox usa e.target.checked.
onSubmit: formularios, preventDefault y envío seguro
Al enviar un formulario, el navegador intenta recargar la página. En React, normalmente quieres evitarlo y manejar el envío con JavaScript. Para eso se usa e.preventDefault() dentro de onSubmit.
function LoginForm() {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [status, setStatus] = React.useState("idle");
async function handleSubmit(e) {
e.preventDefault();
setStatus("loading");
try {
// Simulación de llamada
await new Promise(r => setTimeout(r, 600));
setStatus("success");
} catch (err) {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
autoComplete="email"
required
/>
</div>
<div>
<label htmlFor="password">Contraseña</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
autoComplete="current-password"
required
/>
</div>
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Enviando..." : "Entrar"}
</button>
<p role="status" aria-live="polite">
{status === "success" ? "Acceso concedido" : null}
{status === "error" ? "Hubo un error" : null}
</p>
</form>
);
}Buenas prácticas: usa onSubmit en el <form> (no solo onClick en el botón), y define type="submit" explícitamente. Esto mejora accesibilidad y compatibilidad con teclado.
Pasar handlers como props (comunicación de hijo a padre)
Un patrón común es que el componente padre define el estado y pasa una función al hijo para que el hijo “notifique” eventos. El hijo no necesita conocer cómo se guarda el estado; solo llama al handler.
Ejemplo: lista de tareas con botón en un hijo
function TodoApp() {
const [todos, setTodos] = React.useState([
{ id: 1, text: "Leer", done: false },
{ id: 2, text: "Practicar", done: true }
]);
function toggleTodo(id) {
setTodos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
);
}
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
/>
))}
</ul>
);
}
function TodoItem({ todo, onToggle }) {
return (
<li>
<label>
<input type="checkbox" checked={todo.done} onChange={onToggle} />
{todo.text}
</label>
</li>
);
}Detalles importantes: el padre actualiza estado de forma inmutable (map y { ...t }), y el hijo recibe un handler listo para usar. En el ejemplo, onToggle se pasa como función, no como resultado de una llamada.
Patrones seguros para actualizar estado en handlers
1) Usa actualizaciones funcionales cuando dependes del valor previo
// Bien
setCount(prev => prev + 1);
// Riesgo si hay múltiples actualizaciones seguidas
setCount(count + 1);2) No mutes arrays u objetos; crea nuevas copias
// Mal: mutación
function addItemBad(item) {
items.push(item);
setItems(items);
}
// Bien: copia
function addItemGood(item) {
setItems(prev => [...prev, item]);
}3) Si necesitas el valor actual dentro de un callback asíncrono, evita cierres obsoletos
Un cierre (closure) puede “capturar” un valor antiguo de estado si lo usas más tarde (por ejemplo, en un setTimeout o tras un await). Para contadores o acumulaciones, prefiere la forma funcional.
function DelayedCounter() {
const [count, setCount] = React.useState(0);
function incrementLater() {
setTimeout(() => {
setCount(prev => prev + 1);
}, 1000);
}
return (
<button type="button" onClick={incrementLater}>
Incrementar en 1s (actual: {count})
</button>
);
}Accesibilidad básica en interacción
Botones reales vs. div clicable
Para acciones, usa <button> en lugar de un <div> con onClick. El botón ya incluye soporte de teclado (Enter/Espacio), roles correctos y estados como disabled.
| Necesitas... | Usa... | Evita... |
|---|---|---|
| Acción (enviar, abrir, guardar) | <button type="button"> | <div onClick> |
| Navegación | <a href="..."> (o componente de enlace) | <button> para navegar |
Etiquetas en formularios
Asocia siempre <label> con su control usando htmlFor e id. Esto mejora la usabilidad (clic en la etiqueta enfoca el input) y ayuda a lectores de pantalla.
<label htmlFor="city">Ciudad</label>
<input id="city" value={city} onChange={...} />Mensajes dinámicos anunciables
Para feedback que cambia (por ejemplo, “Guardado”, “Error”), usa una región con role="status" y aria-live="polite" para que tecnologías asistivas lo anuncien sin interrumpir.
<p role="status" aria-live="polite">{message}</p>Errores comunes (y cómo evitarlos)
- Llamar funciones en render:
onClick={doThing()}ejecuta la función al renderizar. Solución: pasa la referenciaonClick={doThing}o envuelve en una funciónonClick={() => doThing(arg)}. - Mutar estado en handlers: hacer
state.push(),state.sort()o modificar propiedades directamente y luego llamar al setter con el mismo objeto/array. Solución: crea copias inmutables ([...prev],prev.map,{...prev}). - Cierres (closures) con valores obsoletos: usar
countdentro de callbacks asíncronos y asumir que es el valor más reciente. Solución: usa setters funcionales (setCount(prev => ...)) o reestructura para leer el valor actual en el momento correcto.