DEV Community

Silviu Technology
Silviu Technology

Posted on

React: UI predecible para reintentos de email

En un flujo de registro, el boton «Reenviar email» parece una pieza pequeña. Pero cuando falla, suele revelar varios problemas de interfaz: el botón queda habilitado durante una espera, el mensaje cambia de sitio, el foco desaparece y la persona pulsa varias veces sin saber que ya hay una solicitud en curso.

Mi enfoque es tratar el reintento como un contrato visual. La interfaz debe explicar cuatro cosas: qué acaba de pasar, si se puede volver a intentar, cuánto falta para hacerlo y dónde encontrar la evidencia. Esto resulta mas útil que añadir otro spinner que gira sin contexto.

Un reintento no es solo un botón

Hay una diferencia importante entre enviar por primera vez y reintentar. El primer envío inicia una acción; el segundo responde a una duda o a un fallo. Si la UI trata ambos casos igual, oculta información que la persona necesita.

Un estado de reintento bien definido puede tener estas fases:

  • idle: todavía no se ha pedido el email o ya terminó la espera.
  • sending: la solicitud está en curso y no debe duplicarse.
  • sent: el servidor aceptó la solicitud.
  • cooldown: existe una espera antes de permitir otro intento.
  • error: la solicitud no pudo completarse y se puede explicar por qué.

La diferencia entre sent y cooldown es especialmente útil. «Enviado» describe el resultado del servidor; «puedes reintentar en 24 segundos» describe la acción disponible. Mezclarlos en una sola frase hace que el mensaje sea dificil de interpretar con lector de pantalla y tambien en una pantalla pequeña.

Para pruebas de signup, una bandeja aislada como tempmailso puede ayudar a revisar cada transición sin mezclar mensajes reales. La interfaz, sin embargo, debe seguir siendo honesta: recibir un email no garantiza que el enlace siga vigente. Incluso una dirección escrita como tepm mail com en un caso de prueba puede descubrir que el formulario muestra un error de formato demasiado tarde.

Modelar el estado antes de pintar la interfaz

Antes de tocar CSS, conviene decidir qué puede ocurrir después de cada evento. Un esquema pequeño evita estados imposibles, como mostrar «Enviado» y un botón activo mientras la misma petición sigue pendiente.

type DeliveryState =
  | { kind: "idle" }
  | { kind: "sending" }
  | { kind: "sent"; retryAfter: number }
  | { kind: "error"; message: string };

function actionLabel(state: DeliveryState) {
  switch (state.kind) {
    case "sending":
      return "Enviando…";
    case "sent":
      return `Reintentar en ${state.retryAfter} s`;
    default:
      return "Reenviar email";
  }
}
Enter fullscreen mode Exit fullscreen mode

El contador no debería ser la unica señal. Si cambia cada segundo y está dentro de una región anunciada, un lector de pantalla puede repetir el mismo dato demasiado. Mejor actualizar el texto accesible solo cuando la acción cambia de fase y dejar el contador visual fuera de aria-live.

Un componente de React con foco y anuncios estables

El botón puede conservar el foco durante la espera. No hace falta moverlo al mensaje cada vez que cambia el estado. Una región aparte anuncia el resultado de forma tranquila:

function RetryEmail({ state, onRetry }: Props) {
  const disabled = state.kind === "sending" ||
    (state.kind === "sent" && state.retryAfter > 0);

  return (
    <section className="delivery-status" aria-labelledby="delivery-title">
      <h2 id="delivery-title">Confirmación del email</h2>
      <p className="delivery-message" aria-live="polite">
        {messageFor(state)}
      </p>
      <button type="button" disabled={disabled} onClick={onRetry}>
        {actionLabel(state)}
      </button>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

En un producto real, messageFor debería recibir el estado del backend y no inferir éxito porque la promesa terminó sin lanzar una excepción. El contrato de la API debe decir si la petición fue aceptada, limitada o rechazada. Ese detalle conecta bien con las alertas de trial con contexto: una señal sirve más cuando explica qué decisión tomar despues.

También revisaría el nombre accesible del botón. «Reenviar» puede ser suficiente al principio, pero «Reenviar email de confirmación» es mas claro si hay varios botones cercanos. No hay que llenar el DOM de instrucciones; solo dar el contexto que se pierde visualmente.

CSS para que el feedback no mueva el formulario

La estabilidad visual es parte de la accesibilidad. Reserva espacio para el mensaje y evita que el botón salte entre estados:

.delivery-message {
  min-height: 2.8rem;
  margin-block: 0.5rem 0.75rem;
  line-height: 1.45;
}

.delivery-status button {
  min-inline-size: 12rem;
}

.delivery-status button:disabled {
  cursor: wait;
  opacity: 0.65;
}
Enter fullscreen mode Exit fullscreen mode

min-height evita que la aparición de una frase nueva desplace el contenido de abajo. min-inline-size hace que «Enviando…» no reduzca el botón para volver a expandirlo cuando aparece el contador. Es un cambio pequeño, pero se nota rapido en móvil.

No usaría color como unica señal. El estado debe existir en texto, y el contraste debe seguir siendo legible en modo oscuro y con zoom. Si el diseño necesita un icono, acompáñalo con una palabra; un check sin etiqueta no explica si el servidor aceptó el envío o si la persona ya puede cerrar la pantalla.

Qué medir después del cambio

Una UI mas tranquila no se demuestra solo con una captura. Registraría:

  • porcentaje de reintentos que terminan en éxito
  • dobles clics durante sending
  • tiempo hasta que aparece el primer mensaje útil
  • abandonos durante el cooldown
  • errores agrupados por causa, no solo por código HTTP

Una conexión lenta puede hacer que el mismo texto se perciba como un error si no se anuncia el estado pendiente. Para pruebas de upgrades, este enfoque encaja con probar upgrades por email sin ruido, porque separa entrega, espera y resultado.

Checklist y preguntas frecuentes

  • ¿El botón se desactiva mientras la solicitud está pendiente?
  • ¿El foco se conserva durante el cambio de mensaje?
  • ¿El lector de pantalla recibe una actualización util sin repetir el contador?
  • ¿El espacio reservado evita layout shift?
  • ¿La API distingue aceptado, limitado y rechazado?
  • ¿El error ofrece una acción concreta?

¿Debo permitir reintentos ilimitados?

No. El límite debe venir de una política de producto y servidor, con un mensaje entendible. La UI puede mostrar la espera, pero no debería fingir que el límite es solo visual.

¿Necesito un modal para confirmar el envío?

Normalmente no. Un mensaje persistente junto al botón es más rapido de entender y conserva el contexto. Usa un modal solo si existe una decisión adicional que realmente requiera atención.

¿Un spinner reemplaza al texto?

No. El movimiento comunica actividad, pero no resultado ni siguiente paso. Combínalo con una etiqueta visible y un anuncio accesible breve.

Un buen reintento no intenta llamar la atención; hace que el siguiente paso sea obvio. En React, modelar primero los estados y reservar espacio para su feedback suele mejorar a la vez la accesibilidad, la percepción de rendimiento y la calidad de las métricas.

Top comments (0)