DEV Community

Silviu Technology
Silviu Technology

Posted on

React: evita dobles envíos al reenviar email

En muchos productos el botón de reenviar verificación parece una tarea menor, pero acaba tocando UX, métricas y soporte a la vez. Si la persona pulsa dos o tres veces porque el estado tarda en reflejarse, el frontend crea ruido innecesario aunque la API esté bien. En equipos de React esto pasa bastante: el evento sale, pero la interfaz no deja claro que ya está trabajando.

Lo he visto sobre todo en flujos donde QA repite pruebas con un correo de usar y tirar o una direccion de correo desechable. Cuando la bandeja tarda unos segundos, la tentación natural es volver a pulsar. Ahí se mezclan eventos, se inflan contadores y luego cuesta saber si el fallo era de entrega, de UI o simplemente de impaciencia humana.

Por que el boton de reenviar suele romper la experiencia

El problema no suele ser un bug enorme. Suele ser una suma de detalles pequeños:

  • el botón cambia de texto tarde
  • el spinner aparece sin reservar espacio
  • el cooldown se muestra en un lugar distinto al foco
  • la telemetría cuenta cada clic aunque el backend ya haya ignorado el duplicado

Eso deja una sensación rara. La persona piensa "no hizo nada", prueba otra vez, y el flujo se vuelve menos confiable de lo que realmente es. En móvil se nota mas, porque el pulgar ya está encima del CTA y el feedback visual llega un poco despues.

También hay un coste interno. Si tu equipo intenta probar correos transaccionales sin mezclar eventos, un botón poco claro arruina esa señal limpia. El frontend no debería fabricar duplicados que luego obligan a explicar dashboards raros.

Que estados merece el CTA desde el primer commit

Para mi, el error es tratar "reenviar email" como un único estado booleano. En realidad hay, como mínimo, cuatro estados útiles:

  1. idle, cuando la acción está disponible.
  2. sending, cuando ya salió la petición.
  3. cooldown, cuando el backend aceptó el reenvío pero quieres frenar intentos inmediatos.
  4. error, cuando toca explicar qué puede hacer la persona sin sonar brusco.

Separar esos estados hace dos cosas buenas. Primero, la interfaz deja de improvisar. Segundo, la medición mejora porque cada transición tiene significado real. Si sólo alternas entre "activo" y "desactivado", terminas leyendo datos medio opacos.

También ayuda a accesibilidad. Un mensaje corto en aria-live puede anunciar "email reenviado, espera 30 s" sin mover el foco ni esconder el botón. Parece un ajuste pequeño, pero hace la interacción bastante mas honesta.

Un patron de React para enfriar el reenvio sin castigar accesibilidad

Prefiero un patrón simple: el botón conserva tamaño, el texto cambia dentro del mismo bloque y el cooldown usa tiempo restante visible. Nada muy fancy, pero funciona bien.

import { useEffect, useState } from "react";

type ResendState = "idle" | "sending" | "cooldown";

export function ResendEmailButton() {
  const [state, setState] = useState<ResendState>("idle");
  const [secondsLeft, setSecondsLeft] = useState(0);

  useEffect(() => {
    if (state !== "cooldown" || secondsLeft === 0) return;

    const timer = window.setTimeout(() => {
      setSecondsLeft((value) => value - 1);
    }, 1000);

    return () => window.clearTimeout(timer);
  }, [state, secondsLeft]);

  useEffect(() => {
    if (state === "cooldown" && secondsLeft === 0) {
      setState("idle");
    }
  }, [state, secondsLeft]);

  async function handleResend() {
    if (state !== "idle") return;

    setState("sending");

    try {
      await resendVerificationEmail();
      setSecondsLeft(30);
      setState("cooldown");
    } catch {
      setState("idle");
    }
  }

  const label =
    state === "sending"
      ? "Enviando..."
      : state === "cooldown"
        ? `Reenviar en ${secondsLeft}s`
        : "Reenviar email";

  return (
    <div className="resend-block">
      <button onClick={handleResend} disabled={state !== "idle"}>
        {label}
      </button>
      <p aria-live="polite" className="resend-status">
        {state === "cooldown" ? "Ya enviamos un nuevo enlace." : ""}
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
.resend-block {
  display: grid;
  gap: 0.5rem;
}

.resend-block button {
  min-inline-size: 12rem;
}

.resend-status {
  min-block-size: 1.5rem;
}
Enter fullscreen mode Exit fullscreen mode

No es un patrón revolucionario, pero evita dos problemas comunes: el doble clic y el layout nervioso. Si además el backend usa idempotencia o rate limiting, mucho mejor. El frontend no reemplaza esa capa; la hace entendible.

Aquí también intento evitar textos ambiguos. "Inténtalo más tarde" suena seco y no enseña nada. "Ya enviamos un nuevo enlace, espera 30 s" da contexto y baja ansiedad. Es una diferencia chica, pero muy util.

Como medir si el cambio realmente mejora el flujo

Yo miraría cuatro señales:

  • tasa de clic duplicado dentro de 10 segundos
  • tiempo hasta el siguiente paso exitoso
  • errores de reenvío por persona única
  • sesiones donde hubo foco perdido o navegación frustrada

Si puedes separar los tests con bandejas dedicadas, todavía mejor. Un equipo que ya sabe validar cohortes sin contaminar bandejas tiene medio camino hecho para leer estas métricas con menos ruido. El truco está en distinguir duplicado real de simple repetición manual durante pruebas.

Cuando QA usa temp gamil com en una nota rápida, no pasa nada. El punto es no dejar que ese tipo de shorthand acabe mezclado con criterios de observabilidad o con nombres de escenarios. Cuanto mas nítido sea el estado del CTA, menos dependes de interpretar capturas a ojo.

Una referencia útil aquí es el patrón de optimistic UI de web.dev. No porque haya que aplicar optimismo puro en un flujo de verificación, sino porque recuerda algo importante: el feedback de la interfaz debe llegar en el momento correcto, no varios beats despues.

Preguntas frecuentes

¿Desactivar el botón no perjudica accesibilidad?

No, si mantienes el contexto visible y anuncias el resultado con una región viva. Lo que perjudica mas es dejar un botón activo que parece disponible cuando en realidad no lo está.

¿Hace falta mostrar el contador?

Yo diría que sí en la mayoría de casos. Reduce incertidumbre y evita la sensación de que el sistema se quedó pensando forever. Además, el tiempo restante ayuda a soporte y QA a entender el comportamiento esperado.

¿Esto mejora performance o solo UX?

Las dos cosas, aunque sea de forma indirecta. Menos clics duplicados significa menos trabajo redundante, menos ruido de analítica y una lectura mas limpia de la salud del flujo.

Top comments (0)