Formularios
Cuando escribes en un input HTML simple, el navegador se encarga del valor por su cuenta: guarda lo que escribiste y redibuja el cursor sin código de tu parte. Conecta el value de ese input a un pedazo de estado y actualiza ese estado en onChange, y el input comienza a funcionar como todo lo demás en React: el estado tiene la verdad sobre qué hay en la página, y el input lo refleja. Eso es un input controlado, 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 pulsación de tecla dispara onChange, que llama a setText con el nuevo valor. El estado y el input se mantienen sincronizados porque React vuelve a renderizar el input con lo que text contiene actualmente. Enviar un formulario recarga la página por defecto, así que handleSubmit llama primero a event.preventDefault(), y luego hace lo que la aplicación necesita, registrando el texto aquí o enviándolo a un servidor en otro lado.
Inputs controlados y no controlados
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 convierte en solo lectura: React mantiene pintando el mismo valor en cada render, y nada de lo que escribas llega al estado, así que empareja los dos siempre que un campo sea controlado. Un input también puede ser no controlado: el DOM mantiene el control de su propio valor, y tú lo lees solo cuando lo necesitas, a través de una ref para un solo campo (una forma de acceder directamente a un elemento del DOM, cubierto completamente en Refs y el DOM) o a través de FormData del formulario para todo el formulario en el momento del envío. Los inputs controlados le dan al componente el valor actual en cada pulsación de tecla, lo que es lo que necesitan la validación en vivo, los contadores de caracteres o los campos que otras partes de la interfaz de usuario dependen de. Los inputs no controlados se saltan un render por pulsación de tecla y acceden al valor solo en el 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 decide si la casilla está marcada: una prop, un manejador de cambios, la misma forma que el input de texto.
Select
Un <select> también toma un value, configurado en el propio select 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 que coincida por ti. No hay necesidad de añadir un atributo selected a 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 la mantiene sincronizada:
function Feedback() {
const [message, setMessage] = useState('')
return (
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
rows={4}
/>
)
}rows establece cuán alto comienza la caja. defaultValue es el equivalente no controlado de value aquí: establece el texto inicial una vez y deja que el DOM lo rastree desde ahí.
Botones de radio
Varios inputs comparten un name, lo 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 él:
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 los lectores de pantalla, y cada label toma una key porque las opciones vienen de un array (listas y claves).
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 acción
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 para llamar a preventDefault(). React detiene la recarga de página predeterminada del navegador por ti.
Object.fromEntries(formData) convierte ese FormData en un objeto simple 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 sobrevive el último valor, y formData.getAll los lee todos.
Los inputs anteriores son no controlados: el DOM mantiene cada valor hasta el envío. Eso importa para el comportamiento que sorprende a la gente que llega desde onSubmit: React reinicia el formulario una vez que la acción se resuelve, borrando los campos no controlados como estos. Un campo controlado se rerenderiza desde el estado y mantiene su valor.
Cada camino se adapta a un tipo diferente de formulario. Una acción se adapta a un formulario cuyo trabajo es reunir valores y entregarlos, usualmente a un servidor, donde la espera es real y el resultado tiene que volver a la interfaz de usuario. Los valores llegan recopilados 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 arrived in React 19. On React 18 and earlier, <form action={handleSignup}> does not throw. React drops the attribute, because a function is not a valid attribute value, and warns about it in development. The form then submits to the current URL, so the page reloads and the function never runs. Submission on those versions goes through onSubmit with event.preventDefault().
value del input viene del estado, y onChange actualiza ese estado. Una vez que eso hace clic 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. A continuación: Levantando el estado, donde dos componentes necesitan compartir la misma pieza de estado.

