Skip to content
This page has been auto-translated and may contain errors.View in English

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:

jsx
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:

jsx
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:

jsx
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:

jsx
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:

jsx
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:

jsx
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().

Los formularios reales raramente se detienen en un campo. Un formulario de registro que pide un nombre, un email y una contraseña podría rastrear tres llamadas useState separadas, pero un objeto de estado y un solo manejador de cambios cubren cualquier cantidad de campos sin que tengas que repetirte:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })

  function handleChange(event) {
    const { name, value } = event.target
    setValues(prev => ({ ...prev, [name]: value }))
  }

  return (
    <form>
      <input name="name" value={values.name} onChange={handleChange} />
      <input name="email" value={values.email} onChange={handleChange} />
      <input type="password" name="password" value={values.password} onChange={handleChange} />
    </form>
  )
}

El atributo name de cada input coincide con una clave en values, así que handleChange lee event.target.name para saber cuál clave actualizar y [name]: value la escribe de vuelta bajo esa misma clave. Añade otro campo al formulario, dale a su input un name que coincida, y el manejador existente ya lo cubre.

La validación puede ejecutarse en dos puntos diferentes, y sirven diferentes propósitos. Validar al cambiar, verificando el nuevo valor dentro de handleChange cuando llega, da retroalimentación de inmediato, útil para algo como un medidor de fortaleza de contraseña. También significa que un error puede aparecer para un campo que el usuario aún no ha terminado de escribir. Validar al enviar, verificando todo el objeto values dentro del manejador de envío antes de que suceda algo con él, espera hasta que el usuario termine, que tiende a ser el mejor predeterminado para las comprobaciones de campo requerido y formato. Muchos formularios mezclan los dos: una verificación ligera al cambiar, un pase completo al enviar.

Un envío que habla con un servidor toma tiempo, y un botón que sigue siendo clickable durante esa espera invita a un segundo clic y un envío duplicado. Rastrea una bandera submitting en el estado, establécela antes de que la solicitud comience, y deshabilita el botón mientras sea verdadera:

jsx
function SignupForm() {
  const [values, setValues] = useState({ name: '', email: '', password: '' })
  const [submitting, setSubmitting] = useState(false)

  async function handleSubmit(event) {
    event.preventDefault()
    setSubmitting(true)
    await saveSignup(values)
    setSubmitting(false)
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* inputs go here */}
      <button disabled={submitting}>{submitting ? 'Submitting...' : 'Sign up'}</button>
    </form>
  )
}

disabled={submitting} desactiva el botón y bloquea los clics mientras la solicitud está en vuelo, luego devuelve el control en el momento en que se resuelve. Un formulario que solo recopila valores y los envía puede entregar todo el trabajo a una action en su lugar, donde useActionState envuelve la acción y te da esa bandera pendiente sin tener que cablear una.

Cada pulsación de tecla en un input controlado hace un viaje de ida y vuelta: el DOM dispara un evento, tu manejador llama a setText, React rerenderiza el componente, y la nueva prop value vuelve al mismo input. Se siente instantáneo, pero el valor mostrado del input está siendo establecido por React en cada render. El elemento del DOM no tiene memoria propia aquí, muestra lo que el estado dice en este momento.

Ese viaje de ida y vuelta también es el costo. Un formulario con muchos campos controlados rerenderiza todo el componente en cada pulsación de tecla en cualquiera de ellos. Usualmente eso es lo suficientemente barato para ignorarlo. Cuando no lo es, o cuando el valor de un campo no necesita afectar nada más hasta el envío, un input no controlado con una ref es la opción más simple:

jsx
function NameForm() {
  const inputRef = useRef(null)

  function handleSubmit(event) {
    event.preventDefault()
    console.log(inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button>Submit</button>
    </form>
  )
}

Sin estado, sin render por pulsación de tecla. defaultValue hace el mismo trabajo aquí que en un textarea. Llega a esto cuando un campo está completamente aislado: nada se renderiza diferente mientras el usuario escribe en él.

useActionState envuelve una acción y devuelve una bandera pendiente junto con lo que la acción devolvió, así que una bandera de envío y un mensaje de error dejan de ser piezas separadas de estado que tú cablea por ti mismo. Server Actions y las integraciones de marco construidas sobre ellos van más lejos, ejecutando la acción misma en el servidor, más allá de donde este manual llega.

JunoEl estado controla el input El patrón a recordar es simple: el 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.
JunoEl estado controla el input Los inputs controlados son el predeterminado: value (o checked para checkboxes) vinculado al estado, onChange actualizándolo, onSubmit con event.preventDefault() manejando el envío. Para un formulario de múltiples campos, un objeto de estado y un handleChange keyed por name reemplazan un montón de llamadas useState separadas, y una bandera submitting mantiene el botón honesto mientras una solicitud está en vuelo. Una función action en el formulario es la otra ruta: React le pasa un objeto FormData y se encarga del evento, y useActionState se hace cargo de esa bandera de envío una vez que un servidor está involucrado.
JunoEl estado controla el input Los inputs controlados son React siendo dueño del valor del DOM en cada render, lo que es lo que te da un valor para leer, validar o derivar en cualquier punto. Sabe cuándo ese viaje de ida y vuelta no está ganándose su sustento y una ref no controlada hará el trabajo, y llega a una action de formulario cuando el trabajo de un formulario es recopilar valores y entregarlos, con useActionState llevando el estado pendiente una vez que un servidor está involucrado.

A continuación: Levantando el estado, donde dos componentes necesitan compartir la misma pieza de estado.