Saltar al contenido
schedule 12 min React

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:

src/components/EventoBasico.jsx
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".

src/components/FormularioReserva.jsx
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:

src/components/FormularioPedido.jsx
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:

src/components/FormularioValidado.jsx
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 e da acceso a e.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.

code

Formulario de reserva de Café Estelar

Intermedio schedule 25 min

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.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.