Formularios
Cuando escribes en un input HTML plano, el navegador maneja el valor por su cuenta: guarda lo que escribiste y redibuja el cursor sin que tengas que escribir código. Conecta el value de ese input a una pieza de estado y actualiza ese estado en onChange, y el input comienza a funcionar como todo lo demás en React: el estado es la fuente de verdad sobre qué hay en la página, y el input lo refleja. Eso es un input controlado, uno donde React decide qué se muestra en cada momento.
Aquí hay un formulario pequeño con un input de texto:
function NameForm() {
const [text, setText] = useState('')
function handleSubmit(event) {
event.preventDefault()
console.log(text)
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button>Submit</button>
</form>
)
}El value del input viene del estado text, y cada tecla que presiones dispara onChange, que llama a setText con el nuevo valor. El estado y el input se mantienen sincronizados porque React re-renderiza el input con el valor que text tiene en ese momento. Por defecto, enviar un formulario recarga la página, así que handleSubmit primero llama a event.preventDefault(), y luego hace lo que la aplicación necesita, registrando el texto aquí, enviándolo a un servidor en otro lugar.
Inputs controlados e incontrolados
El input anterior es un input controlado, porque su valor vive en el estado de React y React decide qué muestra. Dale un value sin un onChange, y el input se vuelve de solo lectura: React sigue pintando el mismo valor en cada renderizado, y nada de lo que escribas llega al estado, así que une los dos siempre que un campo sea controlado. Un input también puede ser incontrolado: el DOM mantiene un registro de su propio valor, y solo lo lees cuando lo necesitas, a través de una ref para un campo individual (una forma de acceder a un elemento del DOM directamente, cubierto completamente en Refs y el DOM) o a través de FormData del formulario para todo el formulario al momento del envío. Los inputs controlados le dan al componente el valor actual en cada tecla, lo que es lo que necesitan la validación en vivo, los contadores de caracteres, o los campos de los que otras partes de la UI dependen. Los inputs incontrolados saltan un renderizado por tecla y llegan al valor solo al momento del envío, lo que es menos código para un campo simple que nada más necesita vigilar.
Checkboxes
Los checkboxes siguen el mismo patrón, pero su estado vive en checked en lugar de value:
function Newsletter() {
const [subscribed, setSubscribed] = useState(false)
return (
<label>
<input
type="checkbox"
checked={subscribed}
onChange={e => setSubscribed(e.target.checked)}
/>
Subscribe to updates
</label>
)
}e.target.checked es un booleano, y subscribed controla si la caja está marcada: una prop, un handler de cambio, la misma forma que el input de texto.
Select
Un <select> también toma un value, establecido en el select mismo en lugar de en la option seleccionada:
function ColorPicker() {
const [color, setColor] = useState('red')
return (
<select value={color} onChange={e => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
)
}Establece value en el select, y React elige la option coincidente por ti. No hay necesidad de añadir un atributo selected en ninguno de los elementos option.
Textarea
En HTML, un textarea lleva su texto entre las etiquetas de apertura y cierre. React lo trata como un input como cualquier otro, así que el texto llega a través de una prop value y onChange lo mantiene sincronizado:
function Feedback() {
const [message, setMessage] = useState('')
return (
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
rows={4}
/>
)
}rows establece qué tan alto comienza la caja. defaultValue es el equivalente incontrolado de value aquí: establece el texto inicial una vez y deja que el DOM lo rastrée desde allí.
Botones de radio
Varios inputs comparten un name, que le dice al navegador que pertenecen juntos. Solo uno puede ser seleccionado. En React el grupo comparte una pieza de estado, y el checked de cada input se compara contra ella:
function ShippingSpeed() {
const [speed, setSpeed] = useState('standard')
return (
<fieldset>
<legend>Shipping speed</legend>
{['standard', 'express', 'overnight'].map(option => (
<label key={option}>
<input
type="radio"
name="speed"
value={option}
checked={speed === option}
onChange={e => setSpeed(e.target.value)}
/>
{option}
</label>
))}
</fieldset>
)
}fieldset y legend etiquetan el grupo para lectores de pantalla, y cada label toma una key porque las opciones vienen de un array (listas y keys).
checked={speed === option} es verdadero para exactamente una opción, así que un valor de estado cubre todo el grupo, mientras que un checkbox lleva su propio booleano.
Envío con una action
Un <form> también puede tomar una función action. React la llama cuando se envía el formulario y le pasa un objeto FormData que contiene los valores del formulario:
function Signup() {
function handleSignup(formData) {
const values = Object.fromEntries(formData)
console.log(values.email, values.password)
}
return (
<form action={handleSignup}>
<input name="email" type="email" />
<input name="password" type="password" />
<button>Sign up</button>
</form>
)
}No hay evento aquí, así que no hay nada en lo que llamar a preventDefault(). React detiene la recarga de página predeterminada del navegador por ti.
Object.fromEntries(formData) convierte ese FormData en un objeto plano en un paso. Las claves vienen del atributo name de cada input, por eso cada input en el formulario necesita uno. Donde varios inputs comparten un nombre y pueden ser enviados juntos, como un grupo de checkboxes, solo el último valor sobrevive, y formData.getAll los lee todos.
Los inputs anteriores son incontrolados: el DOM mantiene cada valor hasta el envío. Eso importa para el comportamiento que sorprende a la gente que llega de onSubmit: React reinicia el formulario una vez que la action se resuelve, borrando campos incontrolados como estos. Un campo controlado se re-renderiza desde el estado y mantiene su valor.
Cada camino se adapta a un tipo diferente de formulario. Una action se adapta a un formulario cuyo trabajo es reunir valores y entregarlos, generalmente a un servidor, donde la espera es real y el resultado tiene que volver a la UI. Los valores llegan reunidos y nombrados. onSubmit con inputs controlados se adapta a un formulario que tiene que reaccionar mientras el usuario escribe, para validación en vivo, un contador de caracteres, o un campo que cambia lo que el resto del formulario muestra.
Version note
Form actions llegó en React 19. En React 18 y anteriores, <form action={handleSignup}> no lanza un error. React descarta el atributo, porque una función no es un valor de atributo válido, y advierte sobre ello en desarrollo. El formulario luego se envía a la URL actual, así que la página se recarga y la función nunca se ejecuta. El envío en esas versiones va a través de onSubmit con event.preventDefault().
value del input viene del estado, y onChange actualiza ese estado. Una vez que eso queda claro para un input de texto, funciona de la misma manera para selects y textareas, y para checkboxes y botones de radio con checked llevando el estado. El envío es su propio paso: el envío de un formulario ejecuta una función que escribes, y esa función decide qué sucede con los valores.
Siguiente: Elevar el estado, donde dos componentes necesitan compartir la misma pieza de estado.

