DEV Community

Silviu Technology
Silviu Technology

Posted on

React: formularios async que respetan el foco

Un formulario que valida un email puede funcionar perfectamente y aun asi sentirse roto. La petición termina, aparece un mensaje, el botón cambia de posición y el cursor acaba en un lugar que la persona no esperaba. En una interfaz pequeña, ese salto parece un detalle. En un flujo de registro repetido muchas veces, se convierte en fricción.

En mis revisiones de frontend suelo tratar el foco como parte del estado de la interfaz. No basta con saber si una validación está idle, checking o invalid: también hay que decidir quién debe recibir la atención del navegador y cuándo. Ese contrato es especialmente util en formularios que consumen APIs, verifican emails o cargan resultados sin navegación completa.

El foco también es estado de la interfaz

Cuando una persona pulsa “Comprobar”, normalmente espera una de tres cosas: que el campo siga listo para corregirse, que aparezca una confirmación clara o que se le lleve a un error que requiere acción. Si el componente mueve el foco en cada respuesta, la experiencia se siente impredecible.

Mi regla es sencilla: una operación de fondo no debe robar el foco por defecto. Si el email es válido, mantengo el cursor donde estaba y anuncio el resultado con texto. Si hay un error en el propio campo, marco el campo y solo muevo el foco cuando el error impediría continuar y la acción es inequívoca.

Esto evita que un lector de pantalla pierda su posición y también ayuda a quien navega con teclado. Es un cambio pequeno de código, pero bastante grande en la sensación de control.

Un flujo async con una sola intención

Un componente no necesita diez booleanos para describir una validación. Un estado explícito hace visible la intención y permite elegir el comportamiento del foco sin adivinar qué combinación de flags está activa.

const [status, setStatus] = useState("idle");
const [message, setMessage] = useState("Escribe un email para continuar.");

async function validateEmail(email) {
  setStatus("checking");
  setMessage("Comprobando el email...");

  try {
    const result = await checkEmail(email);
    setStatus(result.ok ? "valid" : "invalid");
    setMessage(result.ok
      ? "El email está listo para continuar."
      : "Revisa el formato o usa otro email.");
  } catch {
    setStatus("invalid");
    setMessage("La comprobación no terminó. Inténtalo otra vez.");
  }
}
Enter fullscreen mode Exit fullscreen mode

El catch no necesita borrar el valor del campo ni cambiar el foco. La persona puede corregir o reintentar desde el mismo sitio. Para evitar respuestas fuera de orden, asociaría cada petición con el valor que la originó o cancelaría la petición anterior con AbortController. En conexiones lentas, este borde es mas importante que el spinner.

Para probar el lado del servidor, también conviene validar emails de prueba sin ruido. El principio es el mismo en frontend: cada señal debe decir qué pasó y cuál es el siguiente paso.

Mover el foco solo cuando hay un motivo

El HTML puede expresar mucho del contrato sin una librería adicional. El mensaje de estado debe estar cerca del campo y el input debe apuntar a él con aria-describedby.

<label htmlFor="email">Email</label>
<input
  id="email"
  type="email"
  value={email}
  aria-invalid={status === "invalid"}
  aria-describedby="email-message"
  onChange={(event) => setEmail(event.target.value)}
/>
<p id="email-message" aria-live="polite">
  {message}
</p>
Enter fullscreen mode Exit fullscreen mode

aria-live="polite" permite anunciar el resultado sin interrumpir una lectura que ya empezó. No usaría role="alert" para cada estado de carga: convierte una comprobación normal en una emergencia. Cuando el error está en otro panel, por ejemplo un resumen al principio del formulario, sí tiene sentido enfocar ese resumen si contiene el único camino para recuperarse.

También reviso que el foco visible no desaparezca con un reset de estilos. Un outline claro, un orden de tabulación coherente y un botón que conserva su nombre son mas valiosos que una animación vistosa.

Feedback accesible sin mover el layout

Los mensajes de ayuda deben tener un espacio reservado. Si el texto aparece debajo del campo y empuja el botón, cada respuesta async cambia la geometría de la pantalla. Un min-height sencillo reduce el salto y mejora el rendimiento percibido.

.email-message {
  min-height: 1.5rem;
  margin-block: 0.35rem 0.75rem;
}

.email-message[data-status="invalid"] {
  color: #a61b1b;
}

button:focus-visible,
input:focus-visible {
  outline: 3px solid #2457d6;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

El color no debe ser la única diferencia entre éxito y error. Añadir texto y un icono con nombre accesible ayuda a personas con baja visión o daltonismo. Y antes de optimizar con más JavaScript, mediría si el cambio visual es estable; a veces un bloque reservado arregla mas que una abstracción sofisticada.

En una revisión de interfaz me gusta buscar señales con contexto, una idea parecida a detectar drift con contexto útil: el valor no está en marcar cualquier cambio, sino en explicar si requiere una acción.

Cómo probar el contrato en React

Una prueba útil no solo comprueba el texto final. También verifica que:

  • al pasar a checking, el botón se deshabilita sin desaparecer ni cambiar de ancho;
  • una respuesta válida no mueve el foco fuera del input;
  • un error añade aria-invalid y mantiene el mensaje asociado;
  • una respuesta vieja no sobreescribe el resultado de un email mas reciente;
  • el flujo completo se puede recorrer con teclado y tiene foco visible.

Para una prueba de navegador, escribiría un email, iniciaría la petición, cambiaría el valor antes de que responda y liberaría las respuestas en orden inverso. Si el mensaje final corresponde al primer valor, el componente todavía tiene una carrera. Un buzón de pruebas aislado, incluso un correo desechable gratis para un caso manual controlado, puede ayudar a reproducir la espera; los datos sensibles no deben entrar en una prueba compartida.

Durante una sesión de QA también probaría cadenas imperfectas como tem email y tamp mail com. No son formatos recomendados, pero revelan si el mensaje de error explica algo o solo colorea el campo. La validación debe ser estricta cuando corresponde y el feedback debe ser humano.

Preguntas frecuentes

¿Siempre debo enfocar el primer error?

No. En un formulario corto puede ser cómodo, pero en una validación async el foco automático puede interrumpir la edición. Enfócalo cuando el error bloquea el siguiente paso y no existe un destino mas claro.

¿aria-live reemplaza un mensaje visible?

No. Sirve para comunicar cambios a tecnologías de asistencia, pero el mensaje visible sigue siendo importante para todas las personas y para poder revisar el estado sin depender del tiempo de anuncio.

¿Qué tiene que ver esto con un generador de correo temporal?

Las pruebas de registro suelen usar direcciones efímeras y respuestas que tardan o expiran. El producto puede ser distinto, pero el contrato de UI es igual: conservar el contexto, explicar el estado y dejar claro qué acción sigue.

El resultado es una interfaz menos teatral y mas confiable. React gestiona la petición, CSS conserva la geometría y las reglas de accesibilidad protegen la atención de la persona. Cuando esas tres capas comparten el mismo contrato, el formulario async deja de sentirse como una serie de sorpresas.

Top comments (0)