DEV Community

Silviu Technology
Silviu Technology

Posted on

React: estados async accesibles sin saltos

Un formulario de verificación de email puede tener una API rápida y aun asi sentirse lento. El problema suele estar entre la petición y el mensaje que la interfaz muestra: un botón que cambia varias veces, un error que mueve todo el formulario o un spinner que no explica nada. En React, ese pequeño intervalo es parte de la experiencia, no un detalle que se arregla al final.

Cuando reviso una UI de signup, intento que la persona pueda responder tres preguntas sin pensar demasiado: ¿la petición empezó?, ¿qué resultado tuvo? y ¿puedo corregir algo? Un contrato de estados sencillo ayuda a responderlas. También hace más fácil probar emails sin romper estados accesibles sin llenar el componente de booleanos cruzados.

El estado async tambien es parte del diseño

La verificación de email mezcla varias esperas: el servidor valida el formato, un worker envia el mensaje y el usuario puede necesitar volver a la pantalla. Si la UI trata todo como loading, la persona no sabe si debe esperar o revisar su bandeja.

Un correo temporal desechable o una dirección creada para una prueba hace visible el mismo problema. Las pruebas pueden usar un temp mailid o escribir tempail por error, pero la interfaz de producción no debería convertir esos casos raros en mensajes confusos. El objetivo es que el estado sea comprensible, incluso cuando la dependencia externa tarda.

También conviene reservar desde el inicio el lugar del mensaje. El equipo puede medir el Cumulative Layout Shift como una señal de estabilidad, pero no hace falta esperar a un informe de laboratorio para ver el defecto: si el botón baja cada vez que aparece una ayuda, el layout ya está interrumpiendo la tarea.

Un contrato pequeño para cuatro estados

En vez de combinar isLoading, hasError, isVerified y canRetry, prefiero un estado explícito. Para este flujo bastan idle, checking, valid e invalid. Si el producto necesita un estado de expiración, se agrega después como decisión consciente, no como efecto accidental de una promesa que quedó pendiente.

const [status, setStatus] = useState("idle");
const [message, setMessage] = useState("Usa un email al que tengas acceso.");

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

  try {
    const result = await checkEmail(email);
    setStatus(result.ok ? "valid" : "invalid");
    setMessage(result.ok
      ? "Email confirmado. Puedes continuar."
      : "No pudimos confirmar este email. Revisa la dirección.");
  } catch {
    setStatus("invalid");
    setMessage("La comprobación no terminó. Inténtalo otra vez.");
  }
}
Enter fullscreen mode Exit fullscreen mode

La ventaja no es solo la legibilidad. Un estado único evita que una respuesta vieja vuelva a pintar “válido” después de que la persona ya cambió el email. En un componente real, asociaría cada petición con el valor que la originó o cancelaría la anterior con AbortController. Ese borde importa mucho en conexiones lentas, donde las respuestas llegan en un orden que no esperabas.

React: separar datos, feedback y accesibilidad

El mensaje visible y el estado técnico no tienen que ser el mismo campo. checking puede mostrar una ayuda breve, mientras que invalid debe explicar una acción concreta. Para un lector de pantalla, aria-live="polite" anuncia el cambio sin interrumpir una frase que ya está leyendo.

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

No pondría role="alert" para cada cambio de carga. Una alerta agresiva hace que incluso una comprobación normal se sienta urgente. Reservaría el anuncio fuerte para un error que impide continuar, y mantendría el texto de ayuda cerca del campo. Es un detalle pequeno, pero cambia bastante la sensación del formulario.

Si el flujo necesita una acción manual para revisar el buzón, el enlace o botón debe tener un nombre claro. “Continuar” puede ser correcto después de confirmar, pero “Revisar el email recibido” comunica mejor la siguiente acción. La claridad también ayuda a quien usa teclado o zoom.

CSS para que el layout no salte

La solución visual suele ser menos espectacular de lo que parece: reservar altura y evitar transiciones infinitas. El mensaje no debería empujar el botón cuando aparece, y el color no debería ser la única señal del resultado.

.email-hint {
  min-height: 1.5rem;
  margin: 0.35rem 0 0;
  font-size: 0.95rem;
}

.email-hint--checking { color: #5b6472; }
.email-hint--valid { color: #17663a; }
.email-hint--invalid { color: #b42318; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms;
    animation-duration: 0.01ms;
  }
}
Enter fullscreen mode Exit fullscreen mode

Además del color, usaría texto y, cuando aporte valor, un icono con nombre accesible. Si el diseño tiene una barra de progreso, que represente una tarea real; un spinner decorativo no arregla la incertidumbre. La interfaz puede sentirse más rápida al ser estable, aunque la respuesta del servidor sea la misma.

Para datos de prueba, un fake emails generator puede formar parte del entorno aislado, pero no debería ser una dependencia implícita del componente. Define el contrato en el test: qué estado inicial espera, qué ocurre si tarda y cómo se limpia el fixture. Así una prueba de correo temporal para email no termina dictando decisiones de UX.

Que revisar antes de publicar

  • ¿Cada estado tiene un mensaje que explica la siguiente acción?
  • ¿La respuesta de una petición vieja puede sobrescribir la actual?
  • ¿El mensaje está relacionado con el campo mediante aria-describedby?
  • ¿El error se anuncia sin convertir cada carga en una alerta?
  • ¿El espacio del feedback está reservado desde el primer render?
  • ¿La UI sigue siendo clara con teclado, zoom y movimiento reducido?
  • ¿Los tests cubren timeout, reintento y limpieza del buzón?

En la parte operativa, la misma idea de señales claras aparece al validar correos de rollback con señales claras. El frontend no necesita conocer todos los detalles del backend, pero sí necesita un contrato estable para distinguir esperando, confirmado y fallido.

Preguntas frecuentes

¿Debo validar mientras la persona escribe?

Solo si la validación no distrae. Para formato local, espera a que haya una dirección plausible o al blur. Para una petición remota, un pequeño debounce y un mensaje estable suelen ser mas amables que una llamada por cada tecla.

¿Un spinner mejora el rendimiento?

No mejora el tiempo real de red. Puede mejorar el rendimiento percibido si confirma que la acción empezó, pero siempre debe acompañarse con texto y un lugar fijo. Si no, la persona solo ve movimiento.

¿Conviene bloquear el botón?

Bloquéalo solo si repetir la acción puede crear un efecto duplicado. Si la verificación es segura de repetir, permitir un reintento controlado puede ser mejor. La decisión debe salir del contrato de la API, no del miedo a que el usuario haga clic dos veces.

Un buen estado async no hace que la red sea perfecta. Hace que sus imperfecciones sean legibles. Con cuatro estados, un mensaje estable y una comprobación basica de accesibilidad, un formulario de React puede sentirse mucho más confiable sin añadir una capa enorme de abstracción.

Top comments (0)