Eventos y formularios
Ya sabes mostrar datos, pasar props y manejar estado. Ahora necesitas capturar la interacción del usuario: clics, escritura, envío de formularios, teclas. En la sección de JavaScript aprendiste a usar addEventListener — React simplifica todo esto con atributos de evento directamente en el JSX.
Eventos en React
React maneja eventos de forma similar a HTML, pero con algunas diferencias:
// HTML nativo
<button onclick="handleClick()">Clic</button>
// React: camelCase y función (no string)
<button onClick={handleClick}>Clic</button>
Dos diferencias clave: los eventos en React usan camelCase (onClick, no onclick) y reciben una referencia a función, no un string.
Eventos más comunes
// Clic
<button onClick={handleClick}>Clic</button>
// Cambio en input
<input onChange={handleChange} />
// Envío de formulario
<form onSubmit={handleSubmit}>
// Foco y desenfoque
<input onFocus={handleFocus} onBlur={handleBlur} />
// Teclado
<input onKeyDown={handleKeyDown} />
// Ratón
<div onMouseEnter={handleEnter} onMouseLeave={handleLeave} />
El objeto evento
Cada handler recibe un evento sintético de React — un objeto que envuelve al evento nativo del navegador con una API consistente:
function EventoBasico() {
function handleClick(e) {
// e es el evento sintético de React
console.log('Tipo:', e.type) // "click"
console.log('Target:', e.target) // el elemento clickeado
console.log('Posición:', e.clientX, e.clientY)
}
function handleInput(e) {
// e.target.value es el contenido actual del input
console.log('Valor:', e.target.value)
}
return (
<div>
<button onClick={handleClick}>Haz clic aquí</button>
<input onChange={handleInput} placeholder="Escribe algo..." />
</div>
)
}
export default EventoBasico
Funciones inline vs funciones definidas
Puedes pasar funciones de dos formas:
// Función definida (recomendado para lógica compleja)
function handleClick() {
console.log('Clic')
}
<button onClick={handleClick}>Clic</button>
// Función inline (útil para lógica simple)
<button onClick={() => console.log('Clic')}>Clic</button>
// Inline con parámetros
<button onClick={() => agregarAlCarrito(producto.id)}>Añadir</button>
Cuidado:
onClick={handleClick()}(con paréntesis) ejecuta la función inmediatamente. Sin paréntesis,onClick={handleClick}, le pasas la referencia para que React la ejecute cuando haga clic.
preventDefault: evitar comportamiento por defecto
Recuerdas de JavaScript que los formularios recargan la página al enviarse. En React usas e.preventDefault() igual:
function handleSubmit(e) {
e.preventDefault() // Evita que el formulario recargue la página
// Tu lógica aquí
}
<form onSubmit={handleSubmit}>
...
</form>
Formularios controlados
En React, la forma estándar de manejar formularios es con componentes controlados: el valor de cada input viene del estado y se actualiza a través de onChange. El estado es la "fuente de verdad".
import { useState } from 'react'
function FormularioReserva() {
const [nombre, setNombre] = useState('')
const [email, setEmail] = useState('')
const [personas, setPersonas] = useState(1)
const [comentario, setComentario] = useState('')
function handleSubmit(e) {
e.preventDefault()
console.log({ nombre, email, personas, comentario })
alert(`¡Reserva confirmada para ${nombre}!`)
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="nombre">Nombre:</label>
<input
id="nombre"
type="text"
value={nombre}
onChange={(e) => setNombre(e.target.value)}
placeholder="Tu nombre"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
/>
</div>
<div>
<label htmlFor="personas">Personas:</label>
<select
id="personas"
value={personas}
onChange={(e) => setPersonas(Number(e.target.value))}
>
{[1, 2, 3, 4, 5, 6].map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
<div>
<label htmlFor="comentario">Comentario:</label>
<textarea
id="comentario"
value={comentario}
onChange={(e) => setComentario(e.target.value)}
placeholder="Algo que debamos saber..."
/>
</div>
<button type="submit">Reservar</button>
</form>
)
}
export default FormularioReserva
Agrupar estado del formulario en un objeto
Cuando un formulario tiene muchos campos, tener un useState por campo se vuelve repetitivo. Puedes agrupar todo en un solo objeto:
import { useState } from 'react'
function FormularioPedido() {
const [form, setForm] = useState({
nombre: '',
cafe: 'nebulosa',
tamanio: 'mediano',
paraLlevar: false,
})
// Un solo handler para todos los campos
function handleChange(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Pedido:', form)
}
return (
<form onSubmit={handleSubmit}>
<input
name="nombre"
value={form.nombre}
onChange={handleChange}
placeholder="Tu nombre"
/>
<select name="cafe" value={form.cafe} onChange={handleChange}>
<option value="nebulosa">Café Nebulosa</option>
<option value="cosmico">Latte Cósmico</option>
<option value="supernova">Mocha Supernova</option>
</select>
<div>
{['pequenio', 'mediano', 'grande'].map(t => (
<label key={t}>
<input
type="radio"
name="tamanio"
value={t}
checked={form.tamanio === t}
onChange={handleChange}
/>
{t.charAt(0).toUpperCase() + t.slice(1)}
</label>
))}
</div>
<label>
<input
type="checkbox"
name="paraLlevar"
checked={form.paraLlevar}
onChange={handleChange}
/>
Para llevar
</label>
<button type="submit">Pedir</button>
</form>
)
}
export default FormularioPedido
El truco está en el atributo name de cada input, que coincide con la clave del objeto de estado. El handler genérico usa [name] (propiedad computada de JavaScript) para actualizar el campo correcto.
Validación de formularios
La validación en React se hace manteniendo un objeto de errores en el estado:
import { useState } from 'react'
function FormularioValidado() {
const [form, setForm] = useState({ nombre: '', email: '' })
const [errores, setErrores] = useState({})
const [enviado, setEnviado] = useState(false)
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
// Limpiar error del campo cuando el usuario empieza a escribir
if (errores[name]) {
setErrores(prev => ({ ...prev, [name]: null }))
}
}
function validar() {
const nuevosErrores = {}
if (!form.nombre.trim()) {
nuevosErrores.nombre = 'El nombre es obligatorio'
}
if (!form.email.trim()) {
nuevosErrores.email = 'El email es obligatorio'
} else if (!form.email.includes('@')) {
nuevosErrores.email = 'El email no es válido'
}
return nuevosErrores
}
function handleSubmit(e) {
e.preventDefault()
const nuevosErrores = validar()
if (Object.keys(nuevosErrores).length > 0) {
setErrores(nuevosErrores)
return
}
setEnviado(true)
}
if (enviado) {
return <p>¡Reserva confirmada para {form.nombre}!</p>
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="nombre"
value={form.nombre}
onChange={handleChange}
placeholder="Tu nombre"
style={{ borderColor: errores.nombre ? 'red' : '#ddd' }}
/>
{errores.nombre && <p style={{ color: 'red' }}>{errores.nombre}</p>}
</div>
<div>
<input
name="email"
value={form.email}
onChange={handleChange}
placeholder="[email protected]"
style={{ borderColor: errores.email ? 'red' : '#ddd' }}
/>
{errores.email && <p style={{ color: 'red' }}>{errores.email}</p>}
</div>
<button type="submit">Enviar</button>
</form>
)
}
export default FormularioValidado
Comunicación hijo → padre con callbacks
Las props fluyen de padre a hijo, pero ¿cómo le dice un hijo al padre que algo pasó? Pasándole una función callback como prop:
// Componente hijo: llama a la función del padre
function BotonPedido({ onPedir }) {
return (
<button onClick={() => onPedir('Café Nebulosa')}>
Pedir Café Nebulosa
</button>
)
}
// Componente padre: define qué hacer cuando el hijo avisa
function App() {
function handlePedir(cafe) {
alert(`Pedido recibido: ${cafe}`)
}
return <BotonPedido onPedir={handlePedir} />
}
La convención en React es nombrar los callbacks con el prefijo on: onClick, onChange, onPedir, onEliminar.
Resumen
- Los eventos en React usan camelCase (
onClick,onChange,onSubmit) y reciben funciones como valor. - El objeto evento
eda acceso ae.target.value,e.preventDefault()y más. - Los formularios controlados tienen su valor en el estado y se actualizan con
onChange. - Agrupar campos en un objeto de estado con un handler genérico reduce repetición.
- La validación se hace con un objeto de errores en el estado, revisado al enviar.
- Los hijos se comunican con el padre a través de funciones callback pasadas como props.
En la siguiente lección aprenderás listas y renderizado condicional — cómo mostrar arrays de datos con map, por qué las keys importan, y cómo mostrar u ocultar partes de la interfaz según condiciones.
Formulario de reserva de Café Estelar
Crea un formulario de reserva completo para Café Estelar con los siguientes campos:
- Nombre (texto, obligatorio, mínimo 2 caracteres)
- Email (email, obligatorio, debe contener @)
- Fecha (date, obligatorio, no puede ser en el pasado)
- Número de personas (select, 1-8)
- Zona preferida (radio buttons: "Terraza", "Interior", "Barra")
- Comentarios (textarea, opcional)
Requisitos:
- Agrupa todo el estado en un solo objeto con un handler genérico.
- Valida todos los campos obligatorios al enviar. Muestra los errores debajo de cada campo en rojo.
- Limpia el error de un campo cuando el usuario empieza a corregirlo.
- Al enviar correctamente, muestra un mensaje de confirmación con todos los datos de la reserva.
lightbulb Pistas
Usa un solo useState con un objeto para el formulario y otro para los errores. El handler genérico handleChange puede resolver el tipo de input con e.target.type. Para validar la fecha, convierte a Date y compárala con new Date(). Recuerda que los radio buttons usan el mismo name y diferentes value.