Ir al contenido
wannacode

Formularios en un componente

En resumen: Un campo controlado en React guarda su valor en el estado (value y onChange), así que el estado es la única fuente para comprobar y enviar el formulario.

Problema

Escrito, pero vacío

El comprador rellena el pedido y pulsa «Hacer el pedido». El servidor responde que el nombre es obligatorio. Y el campo «Nombre» muestra el nombre.

React

Escribe un nombre, un correo y una dirección, acepta las condiciones y pulsa «Hacer el pedido». El campo conserva «Maria Ivanova», pero la petición lleva name: ''. El correo y la dirección tienen value y onChange, y el nombre no. El código envía el estado, y el estado no sabe lo que hay escrito en el campo.

La idea

Una verdad: el estado

En la lección sobre el DOM el formulario guardaba el texto él mismo, y el código lo leía al enviar. En un componente hay un segundo lugar: el estado. Si los dos no están conectados, cada uno dice lo suyo, como en el ejemplo.

La solución es que el campo no tenga verdad propia. Muestra el valor del estado y en cada tecla le dice al estado el texto nuevo. Un campo así se llama controlado (en inglés, controlled input).

const [name, setName] = useState('');
<input id="name" value={name} onChange={(e) => setName(e.target.value)} />

value={name} es el sentido del estado al campo. onChange es el camino de vuelta: e.target.value es el texto del campo en el momento del evento. En React onChange llega en cada tecla, no solo cuando el campo pierde el foco.

Los demás tipos de campos son iguales, con otra propiedad:

<select value={payment} onChange={(e) => setPayment(e.target.value)}>…</select>
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />

Ahora el estado es la única fuente: lo que hay en él está en el campo, y al revés. El botón se puede desactivar con el estado (!agree), y la petición envía exactamente lo que ve el comprador.

Míralo

Mira el círculo

Una letra recorre todo el camino: campo, evento, estado, nuevo dibujado y otra vez el campo. Después, el mismo campo sin ligadura.

function CheckoutForm() {
const [name, setName] = useState('');
return (
<form onSubmit={submit}>
<label htmlFor="name">Nombre</label>
<input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}
Paso a paso, en palabras
  1. El formulario guarda el nombre en el estado: name = ''. El campo de la página muestra lo que dice el estado, y dice: vacío.
  2. El comprador pulsa M. El navegador pone la letra en el campo y lanza el evento input.
  3. onChange recoge el evento. e.target.value es ahora el texto del campo: «M».
  4. setName("M") cambia el estado. Es el único lugar donde vive el nombre.
  5. El estado cambió, así que React vuelve a llamar a CheckoutForm. Esta devuelve el campo con value = name: «M».
  6. El campo recibe su valor del estado. Es la misma letra, pero ahora el estado decide lo que muestra el campo.
  7. Cada tecla recorre todo el círculo: campo, evento, estado, nuevo dibujado, campo. Una sola fuente de verdad: el estado.
  8. Ahora el mismo campo sin value ni onChange. El campo funciona, pero nadie lo escucha y nadie le da un valor.
  9. El comprador escribe «María». Las letras se quedan en el DOM. El estado sigue diciendo: name = ''.
  10. Al enviar, el código manda el estado: { name: '' }. El servidor responde «El nombre es obligatorio», mientras que el campo muestra «María».
  11. La regla: cada campo tiene value desde el estado y onChange de vuelta a él. Así el campo y el estado no pueden desajustarse.

El círculo parece un rodeo innecesario: la letra está en el campo también sin él. Pero gracias a él el estado y el campo no pueden diferir.

Pruébalo

Liga los campos

El campo Nombre, el select Pago y la casilla Acepto las condiciones no están ligados al estado. Liga los tres. El botón Hacer el pedido está desactivado hasta que se aceptan las condiciones. La comprobación escribe un nombre, un correo y una dirección, elige Tarjeta en la entrega, marca y desmarca la casilla y después hace el pedido. Espera Pedido n.º 61 recibido. y una petición con el nombre escrito y payment: 'card'.

React

Pruébalo

Rellena el hueco

¿Qué va en el hueco para que la casilla quede ligada?

const [agree, setAgree] = useState(false);

<input
  type="checkbox"
  checked={agree}
  onChange={(e) => setAgree(e.target.)}
/>

Pruébalo

Encuentra el error

El comprador pulsa teclas en el campo «Nombre» y no aparece nada en él. El correo funciona. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Una comprobación junto al campo

El servidor comprueba cada pedido. Pero el comprador no debería esperar una respuesta para enterarse de que olvidó la dirección. Así que el formulario también comprueba por sí mismo antes de enviar.

  1. Cuándo. Al pulsar «Hacer el pedido». Si hay errores, no sale ninguna petición.
  2. Qué. Una función check() devuelve un objeto con los errores por campo: { name: 'Escribe tu nombre', email: … }. Un objeto vacío significa «todo bien». Se guarda en el estado errors.
  3. Dónde. Junto al campo, no en una lista común arriba. El comprador ve qué campo debe corregir.
  4. Para todos. Un lector de pantalla no ve que el texto es rojo. Por eso el campo recibe aria-invalid="true" (el lector dice «no válido»), y el mensaje recibe un id al que el campo apunta con aria-describedby. Cuando el foco llega al campo, el lector lee también el mensaje.
<input
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error" className="error">{errors.email}</p>}

El formulario tiene novalidate (en JSX noValidate). Sin él, el navegador detiene el envío con sus propios globos antes de que llegue tu código, y las dos comprobaciones se estorban.

El servidor comprueba otra vez. La comprobación del navegador es una comodidad para el comprador. No protege la tienda: el código de la página está en manos del comprador, y una petición puede llegar también sin él. Por qué es así lo explica la lección «Entrada no confiable».

Pruébalo

Comprueba antes de enviar

Añade una comprobación antes de enviar. Un nombre vacío da Escribe tu nombre, un correo sin @ da El correo debe contener @, una dirección vacía da Escribe una dirección de entrega. Cada mensaje está junto a su campo, en un elemento con el id name-error, email-error y address-error, y el campo tiene aria-invalid=true y aria-describedby apuntando a él. Con errores no sale ninguna petición. La comprobación acepta las condiciones, hace el pedido con el formulario vacío, después con un correo sin @ y al final con todo correcto.

React

La idea

Enviar y la respuesta del servidor

La petición tarda, y el comprador no sabe si su clic llegó. Si vuelve a hacer clic, sale un segundo pedido. Por eso el estado tiene también sending:

  1. Antes de fetch, sending pasa a true. El botón está disabled, y a su lado dice «Enviando…».
  2. Después de la respuesta, sending pasa a false, sea lo que sea lo que haya vuelto.
  3. 201: el cuerpo es { id }. El formulario se sustituye por «Pedido n.º 61 recibido.»
  4. 400: el cuerpo es { errors: { name, email, address, items } }, un mensaje por campo. Van al mismo errors, así que aparecen en los mismos lugares junto a los campos. items no tiene campo: aparece junto a la línea con el carrito.
setSending(true);
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, address, payment, items: ITEMS }),
});
const data = await res.json();
setSending(false);
if (res.ok) setOrderId(data.id);
else setErrors(data.errors);

La comprobación del navegador es más simple que la del servidor: solo busca «@». El correo maria@shop la pasa, y el servidor lo devuelve con «El correo electrónico no es válido». Así que el formulario también debe saber mostrar un error que no conocía.

Más precisamente, para curiosos

Campos no controlados. Un campo sin value guarda el texto él mismo. El valor se toma al enviar: de FormData o con un ref. defaultValue solo da el texto inicial. Es menos código, pero el estado no sabe lo que escribe el comprador hasta que envía. Así que no puedes desactivar el botón según el campo ni comprobar mientras escribe.

De no controlado a controlado. Si value es undefined al principio (useState() sin valor inicial) y más tarde pasa a ser texto, React dice en la consola A component is changing an uncontrolled input to be controlled. El valor inicial de un campo de texto es '', no undefined.

Formularios con action en React 19. <form action={fn}> llama a fn con los FormData del formulario y después vacía los campos no controlados. También hay hooks para ello (useActionState, useFormStatus). Es otro estilo, más cercano al formulario de la lección de HTML. Los campos controlados siguen siendo el camino más directo cuando la interfaz debe reaccionar mientras el comprador escribe.

Un objeto en vez de cinco estados. También puedes tener const [form, setForm] = useState({ name: '', email: '', … }) con una sola función onChange que lea e.target.name. Entonces el objeto nuevo es una copia: setForm({ ...form, [e.target.name]: e.target.value }).

Cuándo comprobar. Al enviar es lo más simple. Una comprobación en cada tecla muestra errores antes de que el comprador termine («el correo no es válido» tras la primera letra). La respuesta habitual: la primera comprobación al enviar o al salir del campo (blur), y después en cada tecla, para que el error desaparezca en cuanto se corrige.

Foco en un error. Tras un envío fallido ayuda mover el foco al primer campo con error. Así el teclado y el lector de pantalla llegan directo al problema.

La comprobación no ve el código. Las tareas de aquí comprueban el comportamiento: qué se muestra, qué está desactivado, qué se envía. No ven si la comprobación está en una función aparte ni cómo se llaman las variables.

Ejercicio

El pedido, hasta el final

Ahora el formulario comprueba en el navegador, pero no muestra que está enviando y mete los errores del servidor en un solo párrafo. Conviértelo en un pedido de verdad: mientras la petición se ejecuta, el botón Hacer el pedido está desactivado y hay un texto Enviando… (con role=status). Ante una respuesta 400, los errores de data.errors van junto a los campos, en los mismos lugares que los tuyos (email-error y aria-invalid en el correo), y el error de items junto a la línea con el carrito. La comprobación escribe el correo maria@shop: pasa tu comprobación, pero el servidor lo devuelve con El correo electrónico no es válido. Después añade .bg y el pedido sale bien: Pedido n.º 61 recibido.

React

Test

Ponte a prueba

1. El campo «Nombre» muestra «María», y el estado name está vacío. ¿Cuál llega al servidor?
2. El campo es <input value={name} />, sin onChange. ¿Qué pasa cuando el comprador escribe?
3. ¿Cómo lees una casilla en onChange?
4. La comprobación del navegador dice que el correo está bien. ¿Debe el servidor comprobarlo otra vez?
5. ¿Por qué el botón «Hacer el pedido» está desactivado mientras se ejecuta la petición?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·