Un formulario de registro no termina cuando el usuario pulsa Crear cuenta. En ese momento empieza una parte delicada: la verificación del email. Si la pantalla cambia demasiado, el botón desaparece o el mensaje de éxito tarda en llegar, la persona no sabe si debe esperar, corregir la dirección o volver a intentarlo.
En interfaces React suelo tratar la verificación como un pequeño flujo de estados, no como un simple isLoading. Este cambio hace que el producto sea más facil de entender, mejora la accesibilidad y evita trabajo visual innecesario. También permite probar cada escenario sin depender de una bandeja real.
El problema: el email también tiene estados
Un flujo de email puede pasar por estados bastante diferentes:
-
idle: todavía no se ha enviado nada. -
sending: la aplicación está enviando la solicitud. -
sent: el mensaje fue aceptado y esperamos al usuario. -
verified: la cuenta ya está confirmada. -
error: algo falló y hay una acción posible.
Cuando todos ellos se representan con el mismo spinner, se pierde contexto. Un spinner dice que hay trabajo, pero no dice qué trabajo, durante cuanto tiempo conviene esperar ni que puede hacer la persona después.
Un correo temporal desechable puede ser útil en pruebas de interfaz, pero el componente debe comportarse igual con cualquier dirección permitida. No conviene diseñar la pantalla alrededor de una bandeja concreta: conviene diseñarla alrededor de una intención observable.
Modelar un estado visible y estable
El estado de la interfaz debería responder tres preguntas: ¿qué pasó?, ¿qué está ocurriendo? y ¿cuál es el siguiente paso? Una forma sencilla de expresarlo es con una unión discriminada en TypeScript:
type EmailState =
| { kind: "idle" }
| { kind: "sending" }
| { kind: "sent"; address: string }
| { kind: "verified" }
| { kind: "error"; message: string };
El campo kind evita combinar por accidente loading: true con un mensaje viejo de error. Es una diferencia pequeña, pero ayuda mucho cuando el formulario crece. La interfaz no deberia adivinar el estado a partir de varios booleanos.
Para mantener el contexto, deja el email visible después del envío y cambia sólo las partes necesarias. Un mensaje como “Enviamos un enlace a ana@example.com” es más útil que “Revisa tu correo”, porque confirma la acción y la dirección que el usuario debe revisar.
Un componente React pequeño, pero honesto
El componente no necesita saber cómo se transporta el email. Sólo necesita recibir el estado y emitir acciones claras:
function VerificationStatus({
state,
onResend,
}: {
state: EmailState;
onResend: () => void;
}) {
if (state.kind === "idle") return null;
if (state.kind === "sending") {
return <p role="status">Enviando el correo de verificación…</p>;
}
if (state.kind === "sent") {
return (
<section aria-labelledby="email-status-title">
<h2 id="email-status-title">Revisa tu bandeja</h2>
<p>Enviamos un enlace a {state.address}.</p>
<button type="button" onClick={onResend}>
Reenviar correo
</button>
</section>
);
}
if (state.kind === "verified") {
return <p role="status">Email verificado. Ya puedes continuar.</p>;
}
return (
<div role="alert">
<p>{state.message}</p>
<button type="button" onClick={onResend}>Intentar de nuevo</button>
</div>
);
}
El botón de reenvío no debería aparecer como un enlace ambiguo. Su nombre comunica la acción y su tipo explícito evita que un cambio posterior lo convierta en un submit accidental. Para los casos donde el mensaje tarda, conserva el contenido del formulario; perderlo después de un clic fallido es una fricción que no aporta nada.
Para flujos con reenvíos, también es útil mostrar reenvíos de verificación con menos fricción y registrar intentos de email sin perder contexto. La observabilidad del producto y la claridad de la UI se refuerzan mutuamente.
Accesibilidad y CSS sin saltos
Un estado dinámico debe anunciarse sin robar el foco. role="status" funciona bien para una actualización informativa; role="alert" es mejor para un error que necesita atención inmediata. No uses ambos para el mismo texto, porque el lector de pantalla podria anunciarlo dos veces.
También evita reservar alturas completamente distintas para cada mensaje. Un bloque con min-height, un ancho razonable y un texto que pueda envolver reduce los saltos de layout. El botón debe conservar un foco visible y no depender sólo del color para indicar que está deshabilitado.
.verification-status {
min-height: 7rem;
max-width: 34rem;
}
.verification-status button:focus-visible {
outline: 3px solid currentColor;
outline-offset: 3px;
}
En móvil, el mensaje puede ocupar dos líneas mas. Eso no es un fallo: es mejor un bloque flexible que truncar la dirección o esconder el siguiente paso.
Cómo probar el flujo
Prueba el componente con una tabla de estados, no sólo con un test de clics:
- El estado
idleno muestra un mensaje vacío. -
sendinganuncia la actividad y conserva la dirección. -
sentmuestra una confirmación específica y un reenvío accesible. -
errorexplica una acción recuperable. -
verifiedno deja visible un botón que ya no hace falta. - El foco de teclado sigue un orden comprensible.
En las pruebas de integración, simula una respuesta lenta y un reintento. Una dirección de prueba como “tempail mail” puede aparecer en datos de QA, pero no la conviertas en lógica especial del componente. El objetivo es comprobar que la interfaz sigue siendo clara cuando el correo no llega enseguida.
Checklist final
- ¿Cada estado tiene un mensaje y una acción comprensibles?
- ¿El usuario conserva el email y el resto del contexto?
- ¿Los cambios se anuncian correctamente a tecnologías asistivas?
- ¿El CSS evita saltos sin fijar una altura frágil?
- ¿Los tests cubren carga lenta, error y reenvío?
- ¿El botón de reenvío tiene límites para no generar acciones repetidas?
Un flujo de verificación bien diseñado no necesita más adornos. Necesita estados honestos, texto útil y una estructura que aguante una respuesta lenta. En React, modelar esas decisiones desde el principio suele costar menos que reparar después una pantalla que hace que cada email parezca un problema distinto.
Top comments (0)