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

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:

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

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

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

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

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

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

Los formularios reales rara vez se detienen en un campo. Un formulario de registro que pide un nombre, un email y una contraseña podría rastrear tres llamadas a useState separadas, pero un objeto de estado y un único handler de cambio cubren cualquier número de campos sin repetirse:

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 qué clave actualizar y [name]: value la escribe bajo esa misma clave. Añade otro campo al formulario, dale a su input un name coincidente, y el handler existente ya lo cubre.

La validación puede ejecutarse en dos puntos diferentes, y sirven para propósitos diferentes. Validar al cambiar, comprobando el nuevo valor dentro de handleChange conforme 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, comprobando todo el objeto values dentro del handler de envío antes de que algo suceda con él, espera hasta que el usuario esté listo, que tiende a ser la opción predeterminada mejor para campos requeridos y comprobaciones de formato. Muchos formularios mezclan los dos: una comprobación ligera al cambiar, un paso completo al enviar.

Un envío que habla con un servidor toma tiempo, y un botón que sigue siendo clickeable durante esa espera invita a un segundo clic y un envío duplicado. Rastreea una bandera submitting en el estado, establécela antes de que la solicitud comience, y desactiva 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 reúne valores y los envía puede delegar todo el trabajo a una action en su lugar, donde useActionState envuelve la action y te da esa bandera pendiente sin tener que conectar una.

Cada tecla en un input controlado hace un viaje de ida y vuelta: el DOM dispara un evento, tu handler llama a setText, React re-renderiza el componente, y la nueva prop value aterroriza en el mismo input. Se siente instantáneo, pero el valor mostrado del input está siendo establecido por React en cada renderizado. 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 re-renderiza todo el componente en cada tecla en cualquiera de ellos. Usualmente eso es bastante barato para ignorar. Cuando no lo es, o cuando el valor de un campo no necesita afectar nada más hasta el envío, un input incontrolado 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 re-renderizado por tecla. defaultValue hace el mismo trabajo aquí que en un textarea. Usa esto cuando un campo está completamente aislado: nada se renderiza diferente mientras el usuario escribe en él.

useActionState envuelve una action y devuelve una bandera pendiente junto con lo que sea que la action devolvió, así que una bandera de envío y un mensaje de error dejan de ser piezas separadas de estado que conectas tú mismo. Las Server Actions y las integraciones de framework construidas sobre ellas van más allá, ejecutando la action misma en el servidor, más allá de donde llega este manual.

JunoEl estado controla el input El patrón en el que enfocarse es pequeño: el 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.

JunoEl estado controla el input Los inputs controlados son la opción predeterminada: value (o checked para checkboxes) atado 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 con clave en name reemplazan un montón de llamadas a 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 renderizado, 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 lugar y una ref incontrolada será suficiente, y opta por una form action cuando el trabajo de un formulario es recopilar valores y entregarlos, con useActionState llevando el estado pendiente una vez que un servidor está involucrado.

Siguiente: Elevar el estado, donde dos componentes necesitan compartir la misma pieza de estado.