<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Silviu Technology</title>
    <description>The latest articles on DEV Community by Silviu Technology (@silviutech).</description>
    <link>https://dev.to/silviutech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013497%2F7d1c82f4-cd78-412d-908d-981280990b57.png</url>
      <title>DEV Community: Silviu Technology</title>
      <link>https://dev.to/silviutech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/silviutech"/>
    <language>en</language>
    <item>
      <title>LLMs: runbooks cortos para usar tools</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:23:57 +0000</pubDate>
      <link>https://dev.to/silviutech/llms-runbooks-cortos-para-usar-tools-23f1</link>
      <guid>https://dev.to/silviutech/llms-runbooks-cortos-para-usar-tools-23f1</guid>
      <description>&lt;p&gt;Cuando un agente con LLM pasa de responder texto a llamar tools, el problema ya no es solo prompt quality. El problema real es operativo: que hace, con que limites, como deja evidencia y que pasa si una dependencia responde raro. En equipos pequenos esto se puede tapar con intuicion por unos dias. En cuanto el flujo toca publishing, CI o cuentas reales, esa intuicion empieza a costar caro.&lt;/p&gt;

&lt;p&gt;Yo prefiero pensar estos sistemas como una cadena corta de decisiones observables. No hace falta un framework pesado. Hace falta un runbook breve, casi un diagrama en palabras, para que cualquiera del equipo entienda entrada, salida, riesgos y fallback. Esa misma idea aparece en patrones de backend como estas &lt;a href="https://dev.to/silviutech/fastapi-colas-visibles-para-emails-largos-5dgd"&gt;colas visibles para trabajos largos&lt;/a&gt;: la gracia no es hacer mas pasos, sino volver el flujo mas legible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que los agentes con tools necesitan runbooks
&lt;/h2&gt;

&lt;p&gt;Un agente suele fallar en tres puntos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;decide usar una tool demasiado pronto&lt;/li&gt;
&lt;li&gt;usa la tool correcta pero sin validar el resultado&lt;/li&gt;
&lt;li&gt;deja una salida bonita para humanos, pero debil para otro sistema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el equipo no describe esos bordes, cada incidente se analiza desde cero. Eso vuelve lenta la operacion y, peor aun, hace dificil mejorar el sistema de manera consistente. Segun el post de Anthropic sobre agentes, separar planificacion, herramientas y criterios de exito reduce ese tipo de ambiguedad operativa (&lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Para mi, un buen runbook de agentes responde cinco preguntas sin drama:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;que objetivo exacto resuelve el paso&lt;/li&gt;
&lt;li&gt;que contexto puede leer&lt;/li&gt;
&lt;li&gt;cuando puede llamar una tool&lt;/li&gt;
&lt;li&gt;que evidencia debe dejar&lt;/li&gt;
&lt;li&gt;como degrada si algo sale mal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Parece simple, pero casi siempre ahi esta el hueco. Muchas pipelines tienen prompts sofisticados y cero reglas sobre evidencia o salida estable. Eso se nota enseguida cuando alguien intenta depurar un fallo a las 2 AM, osea cuando ya es tarde para improvisar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato operativo minimo antes de publicar
&lt;/h2&gt;

&lt;p&gt;Mi version minima cabe en una pantalla. Si no cabe, normalmente el sistema esta mezclando demasiadas responsabilidades.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;objetivo del paso&lt;/li&gt;
&lt;li&gt;inputs permitidos&lt;/li&gt;
&lt;li&gt;tools disponibles y condicion de uso&lt;/li&gt;
&lt;li&gt;formato de salida&lt;/li&gt;
&lt;li&gt;fallback explicito&lt;/li&gt;
&lt;li&gt;nota corta de privacidad o riesgo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un ejemplo practico: si el agente redacta contenido y luego publica, no deberia tener libertad total para reescribir varias veces despues de un error del publicador. Esa frontera tiene que estar escrita. Generar una vez y publicar una vez suena medio obvio, pero es justo el tipo de regla que evita duplicados, cambios espurios y bugs medio pesados.&lt;/p&gt;

&lt;p&gt;Tambien me gusta anotar que datos son "auxiliares pero no centrales". Por ejemplo, si un flujo de QA usa un enlace contextual hacia &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt;, ese dato puede existir como referencia lateral, no como motor del articulo o del sistema. Ese matiz importa porque evita que el agente convierta un detalle SEO en la pieza principal del contenido. Parece una tonteria, pero he visto agentes torcer una buena idea por seguir el keyword y no la intencion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo corto de decision, fallback y trazabilidad
&lt;/h2&gt;

&lt;p&gt;Este es el patron minimo que suelo usar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resumir un cambio y decidir si se publica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed_inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"borrador"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resultado del validador"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"solo si el borrador ya existe y el validador esta en verde"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"artifacts"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"si falla la publicacion, devolver error y no regenerar contenido"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Me gusta porque amarra tres cosas a la vez: el orden de ejecucion, la evidencia minima y el comportamiento ante fallos. Esa estructura combina bastante bien con ideas de estados claros en APIs, como en este enfoque de &lt;a href="https://dev.to/silviutech/fastapi-estados-claros-para-emails-async-40kl-temp-slug-3865389?preview=5780deeac7ff7df71ee58994262f1ce0a7d7d89f8436a9aaf1be9ffb3aaf2d413a29aa58761a9aa4c1dd267984256746c97e193fd7b747c3ad104e74"&gt;estados claros para flujos async&lt;/a&gt;. Cuando el estado esta bien definido, el agente se vuelve mas predecible y el operador no anda adivinando.&lt;/p&gt;

&lt;p&gt;Aqui tambien meto pruebas con ruido. Si el contexto trae cadenas como &lt;code&gt;fake e mail com&lt;/code&gt; o &lt;code&gt;dummy e mail&lt;/code&gt;, el agente no deberia tratarlas como requerimientos de producto. Deberia reconocerlas como texto secundario o ruido semantico. Es una prueba muy barata y bastante util, aunqe mucha gente la salta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs: autonomia frente a operabilidad
&lt;/h2&gt;

&lt;p&gt;La desventaja de este enfoque es clara: le quitas algo de libertad al agente. Un runbook corto no deja tanto espacio para improvisar ni para "resolver" con creatividad una entrada ambigua. Pero la ventaja es mayor en sistemas que tocan dinero, cuentas o distribucion publica.&lt;/p&gt;

&lt;p&gt;Los beneficios que yo suelo ver son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;menos retrabajo despues de un fallo&lt;/li&gt;
&lt;li&gt;mejores handoffs entre quien diseña prompts y quien opera scripts&lt;/li&gt;
&lt;li&gt;incidentes mas cortos porque la evidencia ya existe&lt;/li&gt;
&lt;li&gt;cambios mas seguros cuando cambias proveedor o politica&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La contra principal es que puedes sobre-disenar el flujo. Si cada paso tiene siete validaciones y cuatro formatos de salida, el agente termina lento y el equipo tambien. Mi regla simple: runbook corto para pasos repetibles, libertad mayor para exploracion interna. No es una formula perfecta, pero funciona bastante bien.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementacion
&lt;/h2&gt;

&lt;p&gt;Antes de poner en produccion un agente con tools, revisaria esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;una sola responsabilidad clara por paso&lt;/li&gt;
&lt;li&gt;precondiciones visibles antes de llamar herramientas&lt;/li&gt;
&lt;li&gt;salida consumible por otro sistema, no solo por humanos&lt;/li&gt;
&lt;li&gt;fallback que no regenere ni duplique efectos externos&lt;/li&gt;
&lt;li&gt;artefactos guardados con nombres predecibles&lt;/li&gt;
&lt;li&gt;nota breve sobre datos sensibles o contexto irrelevante&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si ademas quieres mejorar la calidad del debug, guarda decision y artefactos en el mismo run directory. Ese detalle reduce mucho el tiempo de investigacion, sobretodo cuando varias cuentas o cron jobs comparten la misma infraestructura.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>React: ayuda de email sin mover el layout</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:04:48 +0000</pubDate>
      <link>https://dev.to/silviutech/react-ayuda-de-email-sin-mover-el-layout-41ih</link>
      <guid>https://dev.to/silviutech/react-ayuda-de-email-sin-mover-el-layout-41ih</guid>
      <description>&lt;p&gt;En formularios de registro, un detalle pequeno cambia mucho la sensacion de calidad: donde aparece la ayuda del campo email. Si el mensaje entra y sale empujando botones, labels o el siguiente input, la interfaz se siente nerviosa. No parece grave, pero esa vibracion visual hace que la validacion parezca menos confiable, y aveces tambien mas lenta de lo que realmente es.&lt;/p&gt;

&lt;p&gt;En varios equipos frontend he visto el mismo patron: la verificacion async funciona bien, pero el layout no tiene un espacio reservado para el estado. El resultado es un mini CLS local, no siempre medido como una tragedia global, aunque si bastante notorio para la persona usuaria. Cuando eso pasa en signup o recovery, la friccion sube justo donde menos conviene.&lt;/p&gt;

&lt;h2&gt;
  
  
  El salto visual tambien rompe confianza
&lt;/h2&gt;

&lt;p&gt;La ayuda de email suele cambiar entre cuatro estados:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;texto de orientacion inicial;&lt;/li&gt;
&lt;li&gt;cargando una verificacion;&lt;/li&gt;
&lt;li&gt;error de formato;&lt;/li&gt;
&lt;li&gt;confirmacion de que el correo parece usable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si cada estado crea o elimina nodos con distinta altura, el formulario va brincando. Ese brinco distrae, y para teclado o lector de pantalla puede volver menos claro que cosa cambio primero. Segun &lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt;, mantener estabilidad visual ayuda a que la pagina sea mas predecible, algo que en formularios importa un monton.&lt;/p&gt;

&lt;p&gt;Tambien hay un costo de implementacion. Cuando el UI cambia demasiado por un mensaje corto, luego empiezan los parches: margins raros, timeouts cosmeticos y animaciones que tapan el problema real. Me recuerda bastante a la idea de usar &lt;a href="https://dev.to/silviutech/llms-contratos-cortos-para-tool-use-m3e"&gt;contratos cortos para tool use&lt;/a&gt;: menos ambiguedad en el punto de contacto, menos comportamiento raro despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserva una fila estable para ayuda y estado
&lt;/h2&gt;

&lt;p&gt;La solucion mas util que he repetido no es sofisticada: reservar una fila fija debajo del input. Esa fila siempre existe. Cambia el contenido, no el espacio.&lt;/p&gt;

&lt;p&gt;Con eso ganas tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;el boton de submit no se mueve;&lt;/li&gt;
&lt;li&gt;el usuario aprende donde mirar;&lt;/li&gt;
&lt;li&gt;el cambio de estado se vuelve mas facil de anunciar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En CSS suelo empezar con una altura minima y un color neutro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fieldHint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--muted-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.fieldHint&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--danger-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.fieldHint&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"success"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--success-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hace magia, pero evita que el layout se reacomode por una sola linea. Si tu formulario tiene varios checks de red o integraciones, esta constancia vale mucho. En sistemas mas operativos pasa algo parecido con &lt;a href="https://dev.to/alexcarteruk/sre-aisla-correos-de-rollout-en-kubernetes-8mb"&gt;aislar correos de rollout&lt;/a&gt;: cuando cada estado tiene un lugar claro, el flujo entero se entiende mejor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple en React
&lt;/h2&gt;

&lt;p&gt;Aqui va una version chica que me gusta porque separa formato, carga y resultado sin mover la estructura:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Comprobando el correo...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa el formato o usa una direccion valida.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email listo para continuar.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usa un correo que puedas abrir durante el registro.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"fieldHint"&lt;/span&gt; &lt;span class="na"&gt;data-state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo importante no es el snippet exacto. Lo importante es que el contenedor del mensaje ya esta ahi desde el primer render. Luego puedes conectarle debounce, validacion remota o reglas de negocio sin castigar la lectura. Si el flujo necesita probar un inbox de QA para obtener correo temporal, o incluso un &lt;code&gt;temp mailid&lt;/code&gt; en pruebas, el mensaje del campo sigue estable y no convierte cada respuesta async en un temblor visual. Es un detalle chico, pero se nota bastante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que revisar en accesibilidad y rendimiento
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar una tarea asi, yo reviso esta mini lista:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; apunta al texto correcto en todos los estados.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-live="polite"&lt;/code&gt; no dispara mensajes excesivos por cada tecla.&lt;/li&gt;
&lt;li&gt;la altura minima alcanza para idiomas mas largos.&lt;/li&gt;
&lt;li&gt;el color no es la unica senal entre error y exito.&lt;/li&gt;
&lt;li&gt;el mensaje inicial ya ayuda, no solo rellena espacio.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tambien conviene medir si habia cambio de layout antes y despues. No hace falta una ceremonia enorme; con una grabacion corta y Lighthouse suele bastar para ver si la UI quedo mas quieta. La &lt;a href="https://developer.chrome.com/docs/lighthouse/performance/cls" rel="noopener noreferrer"&gt;documentacion de Lighthouse&lt;/a&gt; explica bien por que pequeños desplazamientos pueden afectar la percepcion de fluidez, incluso cuando el formulario "funciona".&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Conviene ocultar la fila vacia hasta que haya feedback?
&lt;/h3&gt;

&lt;p&gt;Yo prefiero no hacerlo. Una fila reservada evita saltos y enseña una posicion fija para la ayuda. Visualmente queda mas calmada, aun si parece un poco mas sobria.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sirve tambien para password o username?
&lt;/h3&gt;

&lt;p&gt;Si, casi siempre. Cualquier campo con validacion async o mensajes cambiantes gana claridad con una zona estable. No es una regla absoluta, pero funciona en la mayoria de formularios reales.&lt;/p&gt;

&lt;h3&gt;
  
  
  Y si necesito mas de una linea?
&lt;/h3&gt;

&lt;p&gt;Entonces sube la altura minima o diseña el campo para dos lineas desde el inicio. Es mejor reservar ese espacio que improvisarlo despues, por que ahi es donde todo se empieza a ver medio roto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;Una UI confiable no siempre necesita mas animacion ni mas logica. Muchas veces necesita menos sorpresa. Reservar una fila para la ayuda del email es una mejora pequena, muy barata, y con impacto real en Accesibilidad, claridad y percepcion de rendimiento. No es glamuroso, pero si quita ruido del camino, ya hizo su trabajo.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>React: estados de espera que no cansan</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:24:16 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-de-espera-que-no-cansan-50e</link>
      <guid>https://dev.to/silviutech/react-estados-de-espera-que-no-cansan-50e</guid>
      <description>&lt;p&gt;En muchos formularios el mayor problema no es la validacion remota, sino la sensacion de que la interfaz duda demasiado. El usuario escribe un email, espera una respuesta del servidor y durante unos segundos no sabe si debe seguir, corregir o simplemente mirar la pantalla. En equipos de producto eso parece un detalle chico, pero en la practica afecta conversion, accesibilidad y confianza. En &lt;code&gt;React&lt;/code&gt;, ese momento merece mas cuidado del que solemos darle.&lt;/p&gt;

&lt;p&gt;Me he encontrado con este patron varias veces en signup y recovery flows: el equipo optimiza la llamada, pero deja el feedback visual para el final. La API responde bien, aunque la UI parpadea, mueve el layout o cambia de tono demasiado pronto. El resultado se siente medio tosco. Si ademas el campo esta ligado a reglas de riesgo o dominios raros como &lt;code&gt;temp mail so&lt;/code&gt;, la tension visual sube un poco mas porque el usuario ya sospecha que algo puede salir mal.&lt;/p&gt;

&lt;p&gt;Nielsen Norman Group lleva anos insistiendo en que la visibilidad del estado del sistema reduce confusion y errores cuando una accion tarda mas de lo esperado (&lt;a href="https://www.nngroup.com/articles/visibility-system-status/" rel="noopener noreferrer"&gt;https://www.nngroup.com/articles/visibility-system-status/&lt;/a&gt;). No hace falta una animacion enorme ni una capa de "microinteractions" por todos lados. Hace falta una senal estable, clara y ubicada donde la persona ya esta mirando.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es la espera, es la incertidumbre
&lt;/h2&gt;

&lt;p&gt;La espera corta casi nunca molesta por si sola. Lo que cansa es la incertidumbre: un spinner que aparece lejos del campo, un mensaje que mueve todo hacia abajo, o una advertencia roja que sale antes de tiempo. Esa combinacion rompe el ritmo de lectura y castiga a quien navega con zoom o lector de pantalla. Tambien mete ruido en mediciones de rendimiento percibido, que a veces importa mas que bajar unos pocos milisegundos del request.&lt;/p&gt;

&lt;p&gt;Cuando reviso una UI asi, suelo preguntar tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿El mensaje ocupa un lugar fijo antes de que exista contenido?&lt;/li&gt;
&lt;li&gt;¿La interfaz diferencia "estoy comprobando" de "esto esta mal"?&lt;/li&gt;
&lt;li&gt;¿La persona puede seguir escribiendo sin sentir que pelea con el formulario?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si una de esas respuestas es no, casi siempre hay una mejora facil. Es parecido a &lt;a href="https://dev.to/hannahdev56/saas-cohortes-de-onboarding-sin-ruido-30nk"&gt;quitar ruido del onboarding sin perder contexto&lt;/a&gt;: no se trata de mostrar mas estado, sino de mostrar el estado correcto en el momento correcto.&lt;/p&gt;

&lt;p&gt;Tambien conviene pensar en el texto de apoyo. He visto equipos dejar notas internas con cadenas como &lt;code&gt;tepm mail com&lt;/code&gt; o &lt;code&gt;tempail mail&lt;/code&gt; para probar coincidencias manuales. No es grave, pero cuando esa clase de texto salta a mocks o a capturas compartidas, la experiencia se vuelve un poco mas desprolija de lo necesario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que cambia cuando el estado de carga ocupa un lugar fijo
&lt;/h2&gt;

&lt;p&gt;Reservar una fila estable para ayuda, carga o error evita dos problemas a la vez: reduce saltos de layout y baja la carga cognitiva. Google recomienda minimizar el &lt;code&gt;Cumulative Layout Shift&lt;/code&gt; porque los cambios inesperados empeoran la experiencia, en especial durante tareas orientadas a conversion (&lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;https://web.dev/articles/cls&lt;/a&gt;). En formularios, eso significa que el mensaje debajo del campo no deberia aparecer empujando botones o moviendo etiquetas.&lt;/p&gt;

&lt;p&gt;El beneficio de producto es mas concreto de lo que parece:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La persona entiende mas rapido si el sistema esta trabajando.&lt;/li&gt;
&lt;li&gt;El mensaje de error llega con menos dramatismo visual.&lt;/li&gt;
&lt;li&gt;El equipo puede medir mejor abandono vs. latencia real.&lt;/li&gt;
&lt;li&gt;La UI se siente mas calma, incluso cuando el backend no va tan rapido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese ultimo punto importa bastante. A veces no puedes hacer milagros en red, pero si puedes evitar que la interfaz transmita panico. Y eso ya es una mejora muy real, aunque suene menos epica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron simple en React para validaciones async
&lt;/h2&gt;

&lt;p&gt;No suelo complicarlo. Un pequeno estado finito suele bastar: &lt;code&gt;idle&lt;/code&gt;, &lt;code&gt;checking&lt;/code&gt;, &lt;code&gt;valid&lt;/code&gt;, &lt;code&gt;invalid&lt;/code&gt;. Lo clave es no mezclar carga con error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usa un email al que tengas acceso.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Comprobando el email...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listo, puedes continuar.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luego la vista reserva siempre el mismo espacio para el hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`email-hint email-hint--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es un patron nuevo, lo se, pero funciona porque mantiene una historia simple. Si necesitas mas sofisticacion, la anades encima; no empiezes con cinco booleanos cruzados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pequenos detalles de CSS que mejoran mucho
&lt;/h2&gt;

&lt;p&gt;La parte visual tambien merece reglas simples. Una altura minima fija, contraste suficiente y transiciones cortas suelen dar mejor resultado que loaders muy decorados.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.email-hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.35rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.95rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.email-hint--checking&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6b7280&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.email-hint--invalid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si hay animacion, prefiero algo discreto y no infinito. Un pulso breve o un cambio de opacidad basta. Tambien revisaria &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, porque este tipo de feedback sale muchisimo y no vale la pena cansar a nadie por una decision puramente estetica. En eso me gusta la misma disciplina que aplicamos al backend cuando buscamos &lt;a href="https://dev.to/alexcarteruk/kubernetes-rota-secretos-sin-reinicios-ciegos-1a7c"&gt;dejar senales claras antes de tocar produccion&lt;/a&gt;: menos magia, mas contexto legible.&lt;/p&gt;

&lt;p&gt;Un ultimo detalle medio olvidado: el microcopy. "Verificando..." suele funcionar mejor que un simple "Cargando...". Parece una diferencia menor, pero le dice a la persona que tarea concreta esta ocurriendo. Es mas honesto, y se siente mejor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Conviene bloquear el boton mientras se valida?
&lt;/h3&gt;

&lt;p&gt;Solo si la accion depende de ese resultado. Si no, prefiero permitir continuar y resolver la regla un paso despues. Bloquear por defecto vuelve la UI mas rigida.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto ayuda al rendimiento real?
&lt;/h3&gt;

&lt;p&gt;No siempre mejora milisegundos reales. Mejora sobre todo el rendimiento percibido, que para formularios largos o sensibles ya es una victoria bastante buena.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuando merece un spinner?
&lt;/h3&gt;

&lt;p&gt;Cuando la espera pasa de lo trivial y necesitas confirmacion explicita. Pero incluso ahi, mejor acompanarlo con texto breve y un lugar fijo. Si no, el usuario adivina demasiado, y eso nunca acaba bien.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>Make Signup Email Tests Less Guessy</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:24:12 +0000</pubDate>
      <link>https://dev.to/silviutech/make-signup-email-tests-less-guessy-155p</link>
      <guid>https://dev.to/silviutech/make-signup-email-tests-less-guessy-155p</guid>
      <description>&lt;p&gt;Signup email tests often fail in a way that annoys everybody. The UI submission works, the API says success, and then the end-to-end check times out waiting for a message that may still be in flight. A lot of teams call that “email flake,” but most of the time the product is not broken. The test is just asking the wrong question at the wrong moment.&lt;/p&gt;

&lt;p&gt;I run into this in both Playwright and broader QA suites. The usual smell is a test that clicks Submit, waits a fixed number of seconds, and then panics when the inbox is empty. That approach feels simple, but it hides useful failure signals. It also makes debugging weird aliases or throwaway addresses like &lt;code&gt;temp gamil com&lt;/code&gt; or &lt;code&gt;tempail&lt;/code&gt; more confusing than it needs to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why signup email tests fail even when the app is fine
&lt;/h2&gt;

&lt;p&gt;The failure is rarely “no email system exists.” It is more often one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app accepted the signup, but the mail job is still queued.&lt;/li&gt;
&lt;li&gt;The test inbox contains an older message from another run.&lt;/li&gt;
&lt;li&gt;The assertion expects the exact subject before the template finishes updating.&lt;/li&gt;
&lt;li&gt;The test never stores enough evidence to tell delay from real breakage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one is the biggie. When a failure report only says “expected email not found,” the team learns almost nothing. Was the user created? Did the job enqueue? Did the provider reject the message? Was the inbox reused? If your answer is “rerun it and see,” the workflow is too fuzzy.&lt;/p&gt;

&lt;p&gt;This is where I like the mindset behind &lt;a href="https://dev.to/sophiax99/reset-emails-need-state-bound-tokens-59mb"&gt;state-bound reset flows&lt;/a&gt;. The point is not just to confirm delivery. The point is to tie the message to the exact state transition your test triggered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wait model I use in Playwright
&lt;/h2&gt;

&lt;p&gt;I prefer a two-stage wait instead of one big sleep.&lt;/p&gt;

&lt;p&gt;First, I wait for the product event that should cause the email. That might be the signup response, a success banner, or a backend event visible through test logs. Second, I poll the inbox with a deadline and narrow matching rules.&lt;/p&gt;

&lt;p&gt;Here is the basic shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check your inbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pollInbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;testAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subjectIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;newerThan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/verify?token=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this helps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;newerThan&lt;/code&gt; filters out stale mail from prior runs.&lt;/li&gt;
&lt;li&gt;Polling every two seconds gives better feedback than a blind 30-second sleep.&lt;/li&gt;
&lt;li&gt;The timeout stays explicit, so the failure tells you how long the system had.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third point sounds small, but it is realy useful in triage. If the test failed after 45 seconds and the queue normally clears in 5, I start looking for a system issue. If it failed after 8 seconds because someone shortened the timeout, that is a test design issue instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small pattern for collecting better evidence
&lt;/h2&gt;

&lt;p&gt;I want every failure to leave behind enough clues for a human to act. My minimum evidence pack is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the generated inbox address,&lt;/li&gt;
&lt;li&gt;the timestamp when signup started,&lt;/li&gt;
&lt;li&gt;the final poll response,&lt;/li&gt;
&lt;li&gt;any message ids found during polling,&lt;/li&gt;
&lt;li&gt;a screenshot of the success state,&lt;/li&gt;
&lt;li&gt;the relevant request or trace id if the app exposes one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That evidence is often more valuable than another rerun. Google’s testing research has repeatedly pointed at the cost of flaky tests and the value of quick diagnosis; a useful overview is in this Testing Blog post on &lt;a href="https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html" rel="noopener noreferrer"&gt;fighting test flakiness&lt;/a&gt;. You do not need Google-scale tooling to apply the lesson. You just need your failure output to be a bit less vague.&lt;/p&gt;

&lt;p&gt;This also pairs nicely with &lt;a href="https://dev.to/ryanlee91/react-invite-emails-without-state-drift-14ho"&gt;invite flows without state drift&lt;/a&gt;. If the app treats invite or verification state clearly, your test can assert on a smaller, cleaner surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I triage failures without rerunning blindly
&lt;/h2&gt;

&lt;p&gt;When a signup email test fails, I go through this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the UI reached the success state.&lt;/li&gt;
&lt;li&gt;Check whether the inbox was unique to the run.&lt;/li&gt;
&lt;li&gt;Compare the poll deadline with normal delivery time.&lt;/li&gt;
&lt;li&gt;Inspect whether a message arrived with the wrong subject or template version.&lt;/li&gt;
&lt;li&gt;Only then decide whether to rerun.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order matters because reruns can hide race conditions. A second pass may “fix” a slow queue, but it teaches the team nothing. I would rather mark the failure as infrastructure-noisy and keep the evidence than pretend the suite is healthier than it is.&lt;/p&gt;

&lt;p&gt;One practical tip: separate “no message arrived” from “message arrived but content was wrong.” Those are different defects, owned by different people, and they should not collapse into the same assertion text. Teams get faster when the failure wording is boringly specific, even if the prose is a little imperfect sometmes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I use one inbox for a whole test file?
&lt;/h3&gt;

&lt;p&gt;Usually no. Per-test inboxes are easier to reason about, especially when the suite runs in parallel. Shared inboxes save a tiny bit of setup and create a lot of doubt later.&lt;/p&gt;

&lt;h3&gt;
  
  
  What timeout is reasonable?
&lt;/h3&gt;

&lt;p&gt;Start from observed delivery time in staging, then add a buffer. If most messages land in 3 to 5 seconds, a 45-second timeout is generous without being silly.&lt;/p&gt;

&lt;h3&gt;
  
  
  When do I treat this as product risk instead of test flake?
&lt;/h3&gt;

&lt;p&gt;When the product event succeeds, inbox isolation is clean, and delivery still misses the deadline often enough to affect releases. At that point the test is doing its job, even if the result is a bit uncomfy.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: reenvios limpios para emails de signup</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:24:27 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-reenvios-limpios-para-emails-de-signup-idf</link>
      <guid>https://dev.to/silviutech/fastapi-reenvios-limpios-para-emails-de-signup-idf</guid>
      <description>&lt;p&gt;En muchos productos el botón de "reenviar email" parece una tarea menor, pero termina creando bastante ruido. El usuario toca dos veces, el frontend reintenta, el worker tarda un poco y el backend acaba enviando dos o tres mensajes que compiten entre sí. Luego soporte no sabe cuál link era el bueno, QA ve resultados inestables y el equipo pierde tiempo en un problema bastnate evitable.&lt;/p&gt;

&lt;p&gt;En FastAPI yo intento resolverlo con un contrato muy chico: un cooldown corto para reenviar, una clave idempotente por intento y un estado consultable desde API. No hace falta una arquitectura exótica. Hace falta dejar claro cuándo se puede reenviar, qué intento sigue vivo y qué evidencia devuelve el sistema si algo tarda más de lo normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde nacen los reenvios duplicados
&lt;/h2&gt;

&lt;p&gt;El problema no suele ser solo "el usuario hizo doble clic". Normalmente aparece por una mezcla de cosas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la UI no sabe si el primer request ya quedó aceptado&lt;/li&gt;
&lt;li&gt;el backend crea un nuevo job aunque el anterior siga activo&lt;/li&gt;
&lt;li&gt;el worker no marca cuál email reemplaza a cuál&lt;/li&gt;
&lt;li&gt;nadie define una ventana mínima antes de permitir otro resend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando eso pasa, el sistema sigue funcionando a medias, pero cada capa interpreta algo distinto. Producto cree que el usuario recibió ayuda. Backend cree que cumplió con el &lt;code&gt;202 Accepted&lt;/code&gt;. Soporte recibe capturas confusas. Y QA, bueno, QA sube el timeout y reza un poco.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeno para reenviar sin caos
&lt;/h2&gt;

&lt;p&gt;Mi versión mínima del contrato tiene cuatro datos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;email_intent_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resend_available_at&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;superseded_by&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con eso ya puedes responder preguntas utiles. ¿Hay un intento vigente? ¿Todavía está dentro del cooldown? ¿El correo anterior fue reemplazado por uno nuevo? ¿Qué intento debe revisar una prueba end-to-end?&lt;/p&gt;

&lt;p&gt;También me gusta guardar un &lt;code&gt;idempotency_key&lt;/code&gt; derivado del usuario y del tipo de flujo, no del clic actual. Así si llegan dos requests casi juntos, la API puede responder con el mismo intento o rechazar el segundo con una explicación clara. No es una bala mágica, pero ordena bastante.&lt;/p&gt;

&lt;p&gt;Si además expones &lt;code&gt;resend_available_at&lt;/code&gt;, el frontend deja de improvisar timers. Ese detalle parece menor, pero baja muchisimo el ruido entre UI y backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo con FastAPI
&lt;/h2&gt;

&lt;p&gt;Este ejemplo enseña bien la idea sin meter demasiada infraestructura:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;active_by_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;COOLDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResendRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup-email/resend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resend_signup_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResendRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;active_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;active_by_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;active_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;available_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend_available_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend cooldown active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_intent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend_available_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend_available_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;intent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend_available_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;superseded_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;active_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;superseded_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent_id&lt;/span&gt;

    &lt;span class="n"&gt;active_by_user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent_id&lt;/span&gt;
    &lt;span class="nf"&gt;enqueue_signup_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_intent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cubre todo, pero ya evita dos errores comunes: aceptar reenvíos infinitos y perder la relación entre intentos viejos y nuevos. Si luego mueves el envío a Celery, RQ o cualquier cola, el contrato puede quedarse casi igual. Eso me gusta porque reduce rehacer la API cuando el volumen crece un poquito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como probar el flujo sin ruido
&lt;/h2&gt;

&lt;p&gt;Aquí conviene separar pruebas de API y pruebas de inbox real. La mayoría de casos deberían validar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;que el segundo resend dentro del cooldown devuelve &lt;code&gt;409&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;que la respuesta incluye &lt;code&gt;resend_available_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;que un intento viejo queda con &lt;code&gt;superseded_by&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;que el intento activo cambia solo una vez&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Las pruebas con inbox real déjalas para unos pocos caminos críticos. Si quieres revisar el correo final en un entorno aislado, herramientas como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; pueden servir para no mezclar bandejas de equipo con automatización de signup. Aun así, yo no pondría esa verificación en cada test. Para la mayoria de suites, basta confirmar estados, timestamps y trazas del intento correcto.&lt;/p&gt;

&lt;p&gt;En esa parte también ayuda usar nombres feos pero reconocibles para escenarios internos, como &lt;code&gt;tempail mail&lt;/code&gt; o &lt;code&gt;dummy e mail&lt;/code&gt;, siempre que esos textos no sustituyan la evidencia real del sistema. La fuente de verdad debe seguir en la API y en tu store de intents.&lt;/p&gt;

&lt;p&gt;Si te ha funcionado construir &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-claros-para-drenados-de-nodo-5e41"&gt;senales claras en cada cambio de estado&lt;/a&gt; en tareas operativas, el mismo principio vale aqui. Y si ya pasaste por &lt;a href="https://dev.to/alexcarteruk/como-validar-correos-tras-un-rollback-en-terraform-3jdg"&gt;validar correos despues de un cambio delicado&lt;/a&gt;, seguramente notarás que los reenvíos necesitan el mismo nivel de contrato, no solo "volver a intentar".&lt;/p&gt;

&lt;p&gt;Google Cloud remarca que los sistemas confiables mejoran cuando cada servicio expone estados observables y objetivos explícitos, porque eso reduce diagnósticos a ciegas &lt;a href="https://cloud.google.com/architecture/devops/devops-tech-test-automation" rel="noopener noreferrer"&gt;source&lt;/a&gt;. No habla solo de email, claro, pero el principio encaja muy bien en este flujo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist corto para dejarlo estable
&lt;/h2&gt;

&lt;p&gt;Esto es lo que yo intentaría dejar listo en el primer PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;un &lt;code&gt;email_intent_id&lt;/code&gt; visible desde la primera respuesta&lt;/li&gt;
&lt;li&gt;cooldown devuelto por API, no calculado solo en frontend&lt;/li&gt;
&lt;li&gt;relación &lt;code&gt;superseded_by&lt;/code&gt; entre intentos&lt;/li&gt;
&lt;li&gt;error &lt;code&gt;409&lt;/code&gt; con contexto legible&lt;/li&gt;
&lt;li&gt;una sola prueba de inbox real por camino crítico&lt;/li&gt;
&lt;li&gt;logs que apunten al intento correcto y no solo al usuario&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es un sistema enorme, pero cambia mucho la operación diaria. Soporte entiende qué pasó. QA deja de pelear con flakes. Backend puede medir cuánto tarda cada resend de verdad. Y el usuario recibe una experiencia más consistente, incluso cuando el proveedor de correo va un poco lento o el worker viene medio cargado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rapidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo borrar el intento anterior?
&lt;/h3&gt;

&lt;p&gt;Yo no lo haría de inmediato. Mejor marcarlo como reemplazado. Ese historial chico ayuda bastante cuando una prueba o un ticket llega unas horas después.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuánto debería durar el cooldown?
&lt;/h3&gt;

&lt;p&gt;Lo suficiente para evitar spam accidental, pero no tanto como para frustrar al usuario. En muchos flujos, 30 a 60 segundos esta bien para empezar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Hace falta inbox real en staging?
&lt;/h3&gt;

&lt;p&gt;Sí, pero poco. Uno o dos checks end-to-end suelen alcanzar. El resto debería probar el contrato de API, porque ahi es donde realmente decides si el resend es sano o caótico.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>React: confirma el email sin romper lectura</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:24:39 +0000</pubDate>
      <link>https://dev.to/silviutech/react-confirma-el-email-sin-romper-lectura-3ih5</link>
      <guid>https://dev.to/silviutech/react-confirma-el-email-sin-romper-lectura-3ih5</guid>
      <description>&lt;p&gt;En muchos formularios el error visible llega tarde. Antes de eso ya hubo una pequeña friccion: el texto se movio, el boton bajo unos pixeles, el mensaje aparecio y desaparecio, y la persona perdio el hilo. En desktop se tolera mas o menos. En mobile, no tanto. Esa lectura rota hace que una validacion correcta se sienta torpe.&lt;/p&gt;

&lt;p&gt;Lo he visto varias veces en equipos frontend. El campo de email tiene buena logica, pero la capa visual responde con demasiado entusiasmo. Si el helper cambia de alto en cada estado, si el spinner entra en la misma fila del label o si el mensaje de error empuja el CTA, la interfaz transmite prisa. &lt;a href="https://www.nngroup.com/articles/form-design-placeholders/" rel="noopener noreferrer"&gt;NN/g&lt;/a&gt; lleva años repitiendo una idea simple: el texto de ayuda debe apoyar la tarea, no competir con ella. Parece obvio, pero en formularios vivos se nos olvida aveces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que la lectura se rompe antes que la validacion
&lt;/h2&gt;

&lt;p&gt;Cuando una persona escribe su correo, hace tres lecturas muy rapidas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;que me piden&lt;/li&gt;
&lt;li&gt;que pasara despues&lt;/li&gt;
&lt;li&gt;si ya puedo seguir&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si esas tres señales cambian de sitio, la interfaz obliga a releer. No es un drama enorme, pero si una suma de microfricciones bastante molesta. &lt;a href="https://baymard.com/blog/inline-form-validation" rel="noopener noreferrer"&gt;Baymard&lt;/a&gt; explica bien que la validacion inline funciona mejor cuando aparece en el momento justo y con una carga visual proporcionada.&lt;/p&gt;

&lt;p&gt;En productos con login, waitlist o onboarding, yo prefiero pensar en "continuidad de lectura" antes que en "feedback inmediato". Es mejor dar una señal clara y estable que cinco señales veloces. La misma logica aparece fuera del frontend puro, incluso en estos &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-claros-para-drenados-de-nodo-5e41"&gt;mensajes operativos claros cuando hay tension&lt;/a&gt;: cuando el mensaje mantiene contexto y tono, la gente decide mas rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  La fila de ayuda fija cambia mas de lo que parece
&lt;/h2&gt;

&lt;p&gt;Una mejora muy poco glamorosa, pero super util, es reservar una fila fija para la ayuda del campo. No para meter texto eterno. Solo para que el layout no baile cada vez que cambia el estado.&lt;/p&gt;

&lt;p&gt;Eso ayuda en tres frentes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mantiene la mirada en el mismo bloque&lt;/li&gt;
&lt;li&gt;evita CLS local dentro del formulario&lt;/li&gt;
&lt;li&gt;reduce la sensacion de que algo "salto" sin permiso&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hace falta una gran arquitectura. Con una sola linea de ayuda persistente puedes pasar de una UI nerviosa a una mucho mas tranquila. Y si tu flujo prueba cuentas internas, alias o algun &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;email temporal para Facebook&lt;/a&gt; durante QA, mejor todavia: el formulario puede explicar el siguiente paso sin editorializar el tipo de correo. Incluso si en datos reales aparecen cadenas raras como &lt;code&gt;temp gamil com&lt;/code&gt; o algun &lt;code&gt;temp mailid&lt;/code&gt;, la interfaz no deberia sonar alarmista por defecto.&lt;/p&gt;

&lt;p&gt;Tambien me gusta porque obliga al equipo a editar el microcopy. Si solo hay una fila, cada palabra tiene que ganarse su lugar. Eso mejora claridad mas que agregar otro iconito, la verdad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron de React para validar sin meter prisa
&lt;/h2&gt;

&lt;p&gt;Este patron separa el estado del campo y el estado del mensaje. La idea no es validar menos, sino comunicar mejor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmailState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintByState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usa un email que puedas abrir en este momento.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;checking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Estamos revisando formato y algunas senales basicas.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa el correo antes de continuar.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Perfecto, ya puedes seguir con este paso.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EmailState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleBlur&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;looksValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;looksValid&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onBlur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleBlur&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`hint hint-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintByState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El detalle importante no es el &lt;code&gt;useState&lt;/code&gt;. Es que el mensaje siempre vive en el mismo sitio. Cambia el contenido, no la estructura. Ese pequeño orden hace que la persona no tenga que perseguir el feedback. En flujos mas de negocio, como una &lt;a href="https://dev.to/hannahdev56/como-validar-correos-de-reactivacion-de-trial-en-un-saas-sin-mezclar-cohortes-4hne"&gt;reactivacion de trial sin mezclar cohortes&lt;/a&gt;, esa consistencia visual evita lecturas dobles justo cuando quieres que el paso sea rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detalles de accesibilidad y rendimiento que si importan
&lt;/h2&gt;

&lt;p&gt;El CSS de apoyo puede ser sorprendentemente pequeño:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5f6876&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint-error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con eso ya ganas bastante, pero yo revisaria ademas estas decisiones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-live="polite"&lt;/code&gt; para no interrumpir en cada tecla&lt;/li&gt;
&lt;li&gt;una altura minima estable para que el boton no rebote&lt;/li&gt;
&lt;li&gt;mensajes distintos para estado neutro y error real&lt;/li&gt;
&lt;li&gt;ningun cambio visual agresivo mientras la persona aun esta escribiendo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://web.dev/articles/optimize-input-delay" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt; conecta este tipo de estabilidad con una percepcion mas rapida de la interfaz. No todo es INP puro, claro, pero menos cambios irrelevantes durante la escritura casi siempre se sienten mejor. Es una mejora pequeña, casi humilde, pero se nota un monton cuando pruebas el flujo con una sola mano en el telefono.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist corto para revisar en mobile
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar una mejora asi, suelo mirar esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el helper explica la accion o el siguiente paso, no repite el label&lt;/li&gt;
&lt;li&gt;el CTA no baja ni sube cuando cambia el mensaje&lt;/li&gt;
&lt;li&gt;el error aparece al perder foco o con una señal suficiente&lt;/li&gt;
&lt;li&gt;el texto sigue claro a 200% de zoom&lt;/li&gt;
&lt;li&gt;el lector de pantalla oye cambios utiles y no puro ruido&lt;/li&gt;
&lt;li&gt;el estado visual de "listo" no parece una alerta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es una checklist heroica, pero ahorra regresiones bastante tontas. Y si tu formulario ya funciona "bien", este tipo de ajuste es de los que suben calidad sin reescribir medio componente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Conviene mostrar validacion apenas aparece la arroba?
&lt;/h3&gt;

&lt;p&gt;Solo si esa señal no mete ansiedad. Confirmar progreso puede ayudar, pero corregir demasiado pronto aveces castiga mas de lo que orienta.&lt;/p&gt;

&lt;h3&gt;
  
  
  Esto reemplaza la validacion del servidor?
&lt;/h3&gt;

&lt;p&gt;No. La UI solo prepara mejor la conversacion con la persona. La validacion real sigue viviendo donde corresponde.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>React: estados vacios para flujos de email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:24:17 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-vacios-para-flujos-de-email-7o</link>
      <guid>https://dev.to/silviutech/react-estados-vacios-para-flujos-de-email-7o</guid>
      <description>&lt;p&gt;En muchos equipos cuidamos bastante el input de registro, pero dejamos medio improvisada la pantalla que viene despues: esa vista donde la persona espera el correo de activacion. Y justo ahi suele aparecer una UX floja. Un cuadro vacio, un spinner eterno, o un mensaje tan ambiguo que parece bug aunque no lo sea.&lt;/p&gt;

&lt;p&gt;He visto este problema varias veces en productos con React. El formulario esta bien, el envio tambien, pero la espera se siente rara. Cuando la interfaz no explica que esta pasando, la gente reintenta, cambia de pestaña o vuelve al paso anterior. Eso mete friccion de mas, y encima complica soporte y analitica.&lt;/p&gt;

&lt;h2&gt;
  
  
  El estado vacio tambien disena la experiencia
&lt;/h2&gt;

&lt;p&gt;Para mi, el estado vacio no es "nada". Es una instruccion silenciosa. Si despues de enviar el formulario la pantalla queda casi blanca, el usuario no sabe si debe esperar, revisar spam o intentar con otro email. Ese vacio no es neutral; empuja decisiones.&lt;/p&gt;

&lt;p&gt;En flujos donde se prueban cuentas con email desechable o correo temporal, esta confusion se nota todavia mas, porque los equipos comparan tiempos de entrega, copys y reglas de validacion al mismo tiempo. Si ademas usas una opcion de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo temporal gratis&lt;/a&gt;, conviene que la UI separe muy claro estos estados: enviado, esperando, recibido y bloqueado.&lt;/p&gt;

&lt;p&gt;Algo parecido pasa cuando revisas integraciones backend con &lt;a href="https://dev.to/silviutech/fastapi-espera-util-para-correos-async-3ojn"&gt;esperas utiles para correos async&lt;/a&gt;. Si el sistema ya tiene una idea razonable del tiempo de espera, el frontend no deberia fingir misterio. Puede decir que busca el mensaje, cuanto suele tardar y cual es el siguiente paso. Parece chico, pero baja ansiedad bastante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que deberia mostrar React antes de que llegue el correo
&lt;/h2&gt;

&lt;p&gt;Mi regla simple es esta: la pantalla de espera necesita contexto, no solo animacion. En React suelo modelarlo con estados explicitos en vez de un boolean suelto tipo &lt;code&gt;isLoading&lt;/code&gt;. Cuando nombras mejor los estados, tambien escribes mejor la interfaz. Suena obvio, pero no siempre se hace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idle | waiting | received | retry | blocked&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailCheckpoint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SuccessPanel&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HelpPanel&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkpoint"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Revisa tu bandeja&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;copyByStatus&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RetryActions&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo importante no es el snippet en si, sino que cada estado tenga una promesa clara. &lt;code&gt;waiting&lt;/code&gt; no dice solo "cargando". Dice "ya enviamos el mensaje". &lt;code&gt;retry&lt;/code&gt; no dice "algo fallo". Dice "todavia no aparece; prueba esto". Esa diferencia hace que la UI parezca mas seria, y honestamente lo es.&lt;/p&gt;

&lt;p&gt;Tambien intento evitar frases vagas tipo "comprueba tu correo". Prefiero algo mas operativo: revisa promociones, espera 30 segundos, vuelve sin recargar, o usa otro proveedor si estas en testing. Cuando el equipo necesita &lt;a href="https://dev.to/hannahdev56/como-revisar-emails-de-activacion-en-saas-1kjf"&gt;revisar emails de activacion&lt;/a&gt;, esas pistas cortas reducen tickets medio bobos.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS pequeno para una espera mas clara
&lt;/h2&gt;

&lt;p&gt;El otro trozo de trabajo esta en CSS. Si el contenedor cambia de altura cada pocos segundos, la espera se siente mas nerviosa de lo que deberia. Yo prefiero reservar una estructura estable para icono, titulo, descripcion y acciones. Nada muy fancy, solo consistente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.checkpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#d9dee8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fbfcfe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.checkpoint__hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#445066&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.checkpoint__actions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese &lt;code&gt;min-height&lt;/code&gt; pequeñito ayuda mucho. Mantiene el bloque estable cuando el texto cambia entre "enviado" y "todavia esperando". No arregla toda la UX, claro, pero evita esa sensacion de interfaz inquieta que nadie describe bien y todos notan un poco.&lt;/p&gt;

&lt;p&gt;Otra cosa que suelo recortar es el abuso de skeletons. Si la espera es de pocos segundos, a veces basta un mensaje directo y una accion secundaria. Un skeleton largo para un simple tem email termina exagerando la complejidad del momento. Y si todo parece enorme, cualquier retraso se siente peor, nomas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad en loading, exito y error
&lt;/h2&gt;

&lt;p&gt;Aqui hay un detalle que se olvida facil: no todos los estados deben anunciarse igual. Si cambias contenido automaticamente, &lt;code&gt;aria-live="polite"&lt;/code&gt; suele funcionar bien para mensajes de progreso. Para errores que bloquean el siguiente paso, a veces conviene una region mas explicita o mover foco solo cuando la accion realmente cambia.&lt;/p&gt;

&lt;p&gt;Tambien reviso tres cosas muy basicas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;que el titulo explique la accion actual&lt;/li&gt;
&lt;li&gt;que el texto no dependa solo del color&lt;/li&gt;
&lt;li&gt;que el boton de reenviar no compita visualmente con esperar un poco mas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando eso falla, la gente martilla el CTA, abre varias pestañas y ensucia la sesion de prueba. Luego alguien ve &lt;code&gt;tamp mail com&lt;/code&gt; en una nota interna o en un valor copiado raro y piensa que el problema fue el proveedor, cuando en verdad era una interfaz poco clara. Pasa mas de lo que deberia, la verdad.&lt;/p&gt;

&lt;p&gt;Si usas React con transiciones o polling suave, intenta que la UI no resetee toda la tarjeta en cada refresh. Actualiza el mensaje, no el mundo entero. Ese detallito hace que la espera se sienta continua, no rota.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A rapido para equipos de producto
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Conviene mostrar countdown?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solo si el sistema tiene una expectativa realista. Si no, mejor una ventana humana tipo "normalmente tarda menos de un minuto". Inventar precision queda medio falso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Y si el usuario usa un proveedor temporal?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No hace falta tratarlo como caso exotico. Lo importante es explicar que el correo puede llegar rapido, caer en otra carpeta o requerir reenvio. La interfaz deberia ayudar, no juzgar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Spinner o mensaje?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Casi siempre mensaje primero. El spinner solo acompaña. Cuando el mensaje es bueno, el spinner deja de cargar con todo el peso narrativo, digamos.&lt;/p&gt;

&lt;p&gt;Al final, este tipo de pantalla parece secundaria, pero define bastante la sensacion del flujo. Si React modela estados claros y CSS mantiene la calma visual, la experiencia mejora sin rediseñar medio producto. Es de esos cambios chicos que no lucen dramaticos en una demo, pero en uso real se sienten enseguida.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Email race checks before flaky test retries</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 01 Sep 2026 02:23:53 +0000</pubDate>
      <link>https://dev.to/silviutech/email-race-checks-before-flaky-test-retries-3lfc</link>
      <guid>https://dev.to/silviutech/email-race-checks-before-flaky-test-retries-3lfc</guid>
      <description>&lt;h1&gt;
  
  
  Email race checks before flaky test retries
&lt;/h1&gt;

&lt;p&gt;When an email test flakes, many teams jump straight to retries. I usually do the opposite. I treat the failure like a timing bug first and a framework problem second. That one habit has saved me more time than another &lt;code&gt;retries: 2&lt;/code&gt; ever did.&lt;/p&gt;

&lt;p&gt;This comes up in signup, OTP, and reset-password coverage where the browser is fine, but the inbox arrives a little late, arrives twice, or gets matched by the wrong test. In bug reports I also keep seeing odd phrases like &lt;code&gt;temp mailid&lt;/code&gt; or &lt;code&gt;temp gamil com&lt;/code&gt;, which is a small clue that the team is already debugging from noisy evidence instead of clean test traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retries hide the real email race
&lt;/h2&gt;

&lt;p&gt;Retries are not useless, but they are very good at masking root cause. A test that passes on the second run still consumed trust from the team. Google noted years ago that flaky tests create meaningful productivity drag across engineering orgs in its post on &lt;a href="https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html" rel="noopener noreferrer"&gt;flaky tests at Google&lt;/a&gt;, and honestly that still feels true in everyday QA work.&lt;/p&gt;

&lt;p&gt;The pattern I see most often looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test A creates an account and waits for any recent email.&lt;/li&gt;
&lt;li&gt;The worker is a bit slow, or a previous message is still visible.&lt;/li&gt;
&lt;li&gt;The assertion finds the wrong mail, or no mail, and fails.&lt;/li&gt;
&lt;li&gt;A retry runs with cleaner timing and suddenly "fixes" it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing was really fixed. The suite just got lucky, which is not the same thing at al.&lt;/p&gt;

&lt;p&gt;If your team has already started building &lt;a href="https://dev.to/silviutech/inbox-contracts-for-stable-playwright-tests-oo9"&gt;stable inbox contracts&lt;/a&gt;, this is easier to spot because the failure surface is smaller. If not, retries can make weak mailbox logic look acceptable for way too long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signals I check before touching retry counts
&lt;/h2&gt;

&lt;p&gt;Before I change any retry setting, I want four signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The exact recipient or alias used by this scenario.&lt;/li&gt;
&lt;li&gt;A correlation value tied to the business event.&lt;/li&gt;
&lt;li&gt;Poll timing with attempt-by-attempt timestamps.&lt;/li&gt;
&lt;li&gt;Enough artifacts to tell whether the app, worker, or test got out of sync.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds simple, but many test harnesses only preserve the final timeout error. From there, people start guessing. They compare timestamps by hand, reopen CI videos, and debate whether the provider was slow or the selector was wrong. It gets messy fast.&lt;/p&gt;

&lt;p&gt;I also like reading adjacent guidance such as &lt;a href="https://dev.to/bitheirstake/disposable-email-checks-need-review-windows-4dbp"&gt;review windows for disposable email checks&lt;/a&gt;, because the review mindset matters. An inbox assertion is not only a yes/no check. It is a small incident timeline, and the timeline should be inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small harness pattern for Playwright and Cypress
&lt;/h2&gt;

&lt;p&gt;The best fix is usually boring: make the mailbox wait narrower and make the evidence richer.&lt;/p&gt;

&lt;p&gt;I prefer a helper that takes the scenario id, the recipient, the expected message type, and a timeout budget. The browser test calls that helper, but the helper owns the polling details. That keeps the framework code thin and the inbox logic consistent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WaitForMailInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;scenarioId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subjectIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WaitForMailInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subjectIncludes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-scenario-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scenarioId&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`mail timeout for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That same shape works in Playwright or Cypress because the framework is not the important part. The contract is. I want the helper to answer a precise question: "Did the email for this scenario arrive within the budget?" Not "Did some inbox somewhere look active?" Those are very diferent questions.&lt;/p&gt;

&lt;p&gt;One subtle improvement helps a lot: log both the matched message and the near misses. If a test expected a verification email but saw two password-reset mails, that is gold for triage. It tells you the system is alive, just not aligned with the scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  A QA checklist for race-proof email tests
&lt;/h2&gt;

&lt;p&gt;Here is the short checklist I use when a team says email tests are flaky:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scope each scenario to a dedicated mailbox or alias.&lt;/li&gt;
&lt;li&gt;Match on business intent, not newest-message order.&lt;/li&gt;
&lt;li&gt;Record polling attempts and inbox counts through the full timeout.&lt;/li&gt;
&lt;li&gt;Save a scenario id that both app logs and test logs can reference.&lt;/li&gt;
&lt;li&gt;Keep retry counts low until the mailbox contract is clean.&lt;/li&gt;
&lt;li&gt;Review the artifacts after the first fail, not after the fifth rerun.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I cannot answer those six points from one CI job, I do not increase retries yet. I tighten observability first. That is usualy where the real win is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Should I remove retries entirely?
&lt;/h2&gt;

&lt;p&gt;No. Retries can still reduce random infrastructure noise. I just do not want them to be the first response to an inbox race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this mostly a Playwright issue?
&lt;/h2&gt;

&lt;p&gt;Not really. I see it in Cypress, API suites, and worker-level smoke tests too. Playwright just makes the sequencing errors easier to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the first artifact worth adding?
&lt;/h2&gt;

&lt;p&gt;An attempt log with timestamps, recipient, and matched subject lines. It is tiny, cheap, and surprsingly effective during triage.&lt;/p&gt;

&lt;p&gt;Reliable email automation usually gets better when the team stops asking for more patience from the test runner and starts asking for better receipts from the mailbox layer. Once that evidence is clean, you can decide whether retries are helping or just hiding the bug for another week.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>cypress</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: una fila fija para validar email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:24:32 +0000</pubDate>
      <link>https://dev.to/silviutech/react-una-fila-fija-para-validar-email-2pd9</link>
      <guid>https://dev.to/silviutech/react-una-fila-fija-para-validar-email-2pd9</guid>
      <description>&lt;p&gt;En muchos formularios React el problema no es validar el email. El problema es que el feedback aparece tarde, ocupa una linea nueva y mueve todo justo cuando la persona esta intentando terminar la tarea. Ese salto parece pequeno, pero en movil se siente muchisimo mas de lo que solemos admitir.&lt;/p&gt;

&lt;p&gt;Me encontre con esto varias veces en flujos de signup y cambios de cuenta. El input se ve limpio, luego aparece un hint, luego cambia a error, luego vuelve a hint, y la interfaz queda medio nerviosa. Si alguien pega algo raro como &lt;code&gt;fake e mail com&lt;/code&gt; o escribe una nota temporal tipo &lt;code&gt;tempail mail&lt;/code&gt;, no necesita una UI dramaticca. Necesita una respuesta clara, estable, facil de corregir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que una sola fila cambia todo
&lt;/h2&gt;

&lt;p&gt;La mejora mas util que he visto es reservar una fila fija para el mensaje desde el primer render. No meter un bloque nuevo cuando hay error, sino conservar siempre el mismo espacio y cambiar solo el contenido. Parece un detalle de CSS, pero tiene impacto en UX, accesibilidad y rendimiento percibido.&lt;/p&gt;

&lt;p&gt;Google explica que el &lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;Cumulative Layout Shift&lt;/a&gt; mide movimientos inesperados durante la carga y la interaccion. En un formulario pequeño no siempre vas a ver un numero dramatico, pero el principio sigue siendo valido: si el boton baja, el ojo pierde referencia y la correccion se vuelve mas lenta. En sesiones de prueba internas esto se nota enseguida, aun cuando el campo "funciona" tecnicamente.&lt;/p&gt;

&lt;p&gt;Tambien hay una razon accesible. Cuando la ayuda y el error viven en el mismo lugar, &lt;code&gt;aria-describedby&lt;/code&gt; apunta siempre a la misma zona. El lector de pantalla no tiene que perseguir nodos que aparecen y desaparecen, y la persona recibe una experiencia mas predecible. Esa continuidad se parece bastante al enfoque de mantener &lt;a href="https://dev.to/silviutech/react-feedback-estable-al-enviar-emails-3gda"&gt;feedback estable mientras se envia un correo&lt;/a&gt;: menos sorpresas, menos carga mental.&lt;/p&gt;

&lt;h2&gt;
  
  
  El patron: reservar espacio antes del error
&lt;/h2&gt;

&lt;p&gt;Mi regla simple es esta:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;la fila de ayuda existe siempre&lt;/li&gt;
&lt;li&gt;el texto cambia segun el estado&lt;/li&gt;
&lt;li&gt;el error fuerte espera a &lt;code&gt;blur&lt;/code&gt; o submit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con eso puedes mostrar una pista amable al principio, confirmar formato cuando haga falta y evitar que cada tecla dispare una mini alarma. Si ademas haces una comprobacion remota, por ejemplo para saber si un dominio de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; debe aceptarse en una demo o en QA, ya tienes un lugar fijo donde explicar el resultado sin empujar el layout.&lt;/p&gt;

&lt;p&gt;No digo que este patron resuelva todo. Si el copy es malo o el criterio del backend cambia cada semana, la interfaz seguira sintiendose rara. Pero reservar esa fila te quita una clase entera de friccion, y eso ya es un win bastante serio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo pequeno en React y CSS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isEmailLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\S&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;blurred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBlurred&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blurred&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isEmailLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;invalid&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa el formato. Usa nombre@dominio.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usaremos este correo para acceso, avisos y recuperacion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onBlur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setBlurred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;invalid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;invalid&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hint hintError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5f6b7a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hintError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo clave aca es &lt;code&gt;min-height&lt;/code&gt;. Esa propiedad parece humilde, pero hace casi todo el trabajo visual. Luego &lt;code&gt;aria-live="polite"&lt;/code&gt; ayuda a anunciar cambios sin interrumpir de mas, y &lt;code&gt;aria-describedby&lt;/code&gt; mantiene la relacion semantica. Cuando despues agregas validacion asincrona, puedes reutilizar esa misma linea para estados como "comprobando" o "dominio no permitido". Ese enfoque combina muy bien con tener &lt;a href="https://dev.to/silviutech/react-feedback-de-email-sin-romper-el-foco-18i7-temp-slug-6950159?preview=23857a09c5e6d489f4559d22b0c799c330e564c118031e9967c09556c7319ad78d83c5aa93add5d376dd2d8282a8f541590973895b74ecc3f436ac29"&gt;mensajes de email sin romper el foco&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Si necesitas pruebas manuales para dominios poco comunes o cuentas desechables, una direccion de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo desechable&lt;/a&gt; puede servir para validar copys, tiempos y estados sin mezclar cuentas reales. El punto no es promover un truco, si no reducir ruido cuando el equipo esta probando variantes y necesita respuestas visibles rapido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde se nota en accesibilidad y rendimiento
&lt;/h2&gt;

&lt;p&gt;La mejora se nota en tres sitios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;el ojo conserva la posicion del boton y del siguiente campo&lt;/li&gt;
&lt;li&gt;el mensaje ayuda antes de castigar&lt;/li&gt;
&lt;li&gt;el lector de pantalla encuentra siempre el mismo destino&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No hace milagros, obvio. Pero si el form tiene varios campos parecidos, quitar esos movimientos reduce bastante la sensacion de desorden. Baymard lleva años mostrando que los formularios pierden conversion por detalles de feedback y claridad, no solo por reglas complejas de negocio &lt;a href="https://baymard.com/blog/checkout-usability" rel="noopener noreferrer"&gt;en sus estudios de checkout&lt;/a&gt;. Esa parte a veces se subestima porque no sale como error en logs, pero el usuario la siente igual.&lt;/p&gt;

&lt;p&gt;Yo intentaria revisar dos cosas antes de darlo por cerrado: zoom alto en iPhone y autocompletado en Chrome. Si en ambos casos la fila sigue estable y el mensaje sigue legible, el patron ya esta bastante maduro. Si no, normalmente el ajuste es pequeño y merece la pena hacerlo temprano. Despues todo se mezcla con mas estados, mas analitica y mas prisa, y ahi se vuelve un arreglo mas incomodo de lo que deberia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rapidas
&lt;/h2&gt;

&lt;p&gt;Q: ¿Conviene validar en cada tecla?&lt;br&gt;&lt;br&gt;
A: Solo si el mensaje no castiga ni distrae. En muchos casos, &lt;code&gt;blur&lt;/code&gt; deja una experiencia mas limpia.&lt;/p&gt;

&lt;p&gt;Q: ¿Sirve esto si luego llamo a una API?&lt;br&gt;&lt;br&gt;
A: Si, por que la fila fija absorbe el estado remoto sin meter otro salto visual.&lt;/p&gt;

&lt;p&gt;Q: ¿Hace falta otra libreria?&lt;br&gt;&lt;br&gt;
A: No realmente. Con React base y CSS sencillo ya resuelves una parte grande del problema.&lt;/p&gt;

&lt;p&gt;En frontend hablamos mucho de performance como milisegundos, bundles y Lighthouse. Todo eso importa, claro. Pero una validacion estable tambien es rendimiento percibido. Cuando el formulario no tiembla, no regaña antes de tiempo y conserva una sola zona de ayuda, se siente mejor. Y se nota mas de lo que parece, incluso si el cambio fue bien pequeñito.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
    <item>
      <title>Playwright Retries Need Inbox Evidence</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:24:03 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-retries-need-inbox-evidence-39bc</link>
      <guid>https://dev.to/silviutech/playwright-retries-need-inbox-evidence-39bc</guid>
      <description>&lt;p&gt;I do not mind retries in Playwright. I mind retries that erase the evidence I needed. That is a very different problem. When a signup or reset-email test fails, the fastest way to waste a morning is to rerun it three times and keep only the last error message.&lt;/p&gt;

&lt;p&gt;In QA teams, email steps often sit behind a temp mail inbox or a fake email address created just for the run. The browser flow may be correct, yet the retry still feels random because the test never records what inbox it used, what timestamp boundary it applied, or which message it actually opened. After a few weeks, people start saying the email layer is flaky when the real problem is thinner evidence.&lt;/p&gt;

&lt;p&gt;This post builds on earlier ideas around &lt;a href="https://dev.to/silviutech/playwright-email-tests-need-inbox-contracts-3g17"&gt;inbox contracts for Playwright email tests&lt;/a&gt; and &lt;a href="https://dev.to/silviutech/lease-inboxes-in-parallel-playwright-tests-1n3l"&gt;parallel inbox isolation in Playwright&lt;/a&gt;. The extra step here is simple: every retry should leave behind enough inbox evidence that a human can explain the failure in one read, not after guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retries hide the real email bug
&lt;/h2&gt;

&lt;p&gt;A retry looks helpful because it gives you another chance to pass. But if the first attempt used the wrong inbox, matched an older message, or crossed a timing boundary, the second attempt can quietly overwrite the story. You end up with a green run and a bad test, or a red run with no reason why.&lt;/p&gt;

&lt;p&gt;That shows up a lot in suites that use temp mail helpers copied between repos. One spec waits for "latest email", another filters by subject only, and a third logs almost nothing. The reports feel inconsistant even when the product bug is the same.&lt;/p&gt;

&lt;p&gt;The fix is not fancy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;record the inbox address&lt;/li&gt;
&lt;li&gt;record the trigger time&lt;/li&gt;
&lt;li&gt;record the list of matching messages seen during polling&lt;/li&gt;
&lt;li&gt;record why the chosen message passed the ownership check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of your teammates types tempail mail into a note while triaging, that is usually a clue the debugging flow is rushed already.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence bundle I want from every retry
&lt;/h2&gt;

&lt;p&gt;For each attempt, I want an evidence bundle that answers five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which inbox or alias belonged to this attempt?&lt;/li&gt;
&lt;li&gt;When did the app action happen?&lt;/li&gt;
&lt;li&gt;Which messages were observed after that point?&lt;/li&gt;
&lt;li&gt;Why did the helper accept or reject each candidate?&lt;/li&gt;
&lt;li&gt;What URL or token was extracted in the end?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same diagnostic thinking QA engineers already use for API tests. We keep request ids, response bodies, and timing. Email assertions deserve the same treatment, even if the setup feels more "UI-ish" at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright helper that records inbox evidence
&lt;/h2&gt;

&lt;p&gt;I prefer putting the evidence logic in one shared helper rather than inside every test. That keeps retries boring, which is good. Boring code is easier to trust.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InboxEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;acceptedMessageId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rejectionNotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerificationMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InboxEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;rejectionNotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rejectionNotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`skip &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: wrong subject`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rejectionNotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`skip &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: ownership proof missing`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acceptedMessageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter a bit more than the rest. First, the poll starts from &lt;code&gt;triggeredAt&lt;/code&gt;, not "whenever the inbox was created." Second, the thrown error includes structured evidence. That makes retry output much more usefull in CI logs and report attachments.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read the evidence during triage
&lt;/h2&gt;

&lt;p&gt;When a retry fails, I scan the evidence in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;matched&lt;/code&gt; is empty: likely delivery delay or wrong inbox wiring&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;matched&lt;/code&gt; has older mail only: timestamp boundary is wrong&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;matched&lt;/code&gt; has fresh mail but no accepted id: ownership proof is too weak&lt;/li&gt;
&lt;li&gt;accepted id exists but downstream step fails: the product or parser is probably wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sequence keeps the review calm. Instead of saying "email failed again", you get a narrower claim. Narrow claims are what make flaky work survivable, especialy when several pipelines are red at once.&lt;/p&gt;

&lt;p&gt;If you want a lightweight benchmark for why logs matter, Google’s SRE material repeatedly frames observability as the thing that reduces mean time to resolution, not just alerting noise. That principle maps cleanly here too: better evidence shortens triage loops, even for humble test inboxes. Source: &lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;https://sre.google/sre-book/monitoring-distributed-systems/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist for reliable retries
&lt;/h2&gt;

&lt;p&gt;Before I accept an email retry strategy, I check these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every retry gets a fresh inbox or a provably isolated alias&lt;/li&gt;
&lt;li&gt;polling starts after the user action, not before&lt;/li&gt;
&lt;li&gt;the helper saves message ids and receive times&lt;/li&gt;
&lt;li&gt;ownership proof is checked in body, subject, or metadata&lt;/li&gt;
&lt;li&gt;the failure output can stand on its own in CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most of those are missing, the retry is just a second roll of the dice. It may still pass, but it will not teach you much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I fail fast instead of retrying?
&lt;/h3&gt;

&lt;p&gt;Not always. Retries are fine when the system has normal delivery variance. I just want the retry to preserve the first attempt's evidence so the signal does not vanish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need this for every email test?
&lt;/h3&gt;

&lt;p&gt;No. I start with signup, reset-password, and invite flows because those are the ones where hidden ambiguity hurts the most.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my inbox vendor only returns the latest message?
&lt;/h3&gt;

&lt;p&gt;Then I would wrap that limitation with stronger logging and stricter ownership checks. It is not ideal, but you can still make the failure story much less fuzzy.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: calma visual para checks async</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 30 Aug 2026 23:23:56 +0000</pubDate>
      <link>https://dev.to/silviutech/react-calma-visual-para-checks-async-4d8l</link>
      <guid>https://dev.to/silviutech/react-calma-visual-para-checks-async-4d8l</guid>
      <description>&lt;p&gt;Cuando un formulario revisa un email contra una regla remota, muchas interfaces se ponen raras demasiado pronto. Aparece un spinner, el boton cambia de tamano, el helper text desaparece y vuelve, y la persona siente que el campo esta peleando con ella. En frontend, ese detalle pesa bastante mas de lo que parece.&lt;/p&gt;

&lt;p&gt;En equipos donde trabajamos signup, soporte y analitica a la vez, el problema casi nunca es la llamada async por si sola. El problema es el ritmo visual que le ponemos encima. Si un usuario escribe rapido, borra una letra o pega un valor temporal tipo &lt;code&gt;temp gamil com&lt;/code&gt;, la UI puede entrar en una secuencia medio caotica: pendiente, warning, ok, pendiente otra vez. Todo es tecnicamente correcto, pero la experiencia sale algo torpe.&lt;/p&gt;

&lt;p&gt;Segun Google, los cambios inesperados de layout siguen afectando la sensacion de calidad y la lectura del flujo (&lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt;). Y Nielsen Norman Group lleva tiempo remarcando que los mensajes de estado deben apoyar la tarea, no competir con ella (&lt;a href="https://www.nngroup.com/articles/error-message-guidelines/" rel="noopener noreferrer"&gt;NN/g&lt;/a&gt;). Para mi, ese es el punto de partida.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es la llamada remota, es el ritmo visual
&lt;/h2&gt;

&lt;p&gt;Una comprobacion async de email suele existir por motivos validos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detectar dominios desechables o de baja confianza&lt;/li&gt;
&lt;li&gt;preparar una advertencia antes del submit&lt;/li&gt;
&lt;li&gt;evitar reglas duplicadas entre frontend y backend&lt;/li&gt;
&lt;li&gt;dar una pista temprana sin bloquear el flujo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nada de eso esta mal. Lo que si veo mal seguido es mezclar todos esos objetivos dentro del mismo segundo visual. El usuario escribe, el campo lanza una request, el texto de ayuda cambia tres veces, y el CTA sube o baja unos pixeles. Ese mini-desorden rompe confianza aunque el formulario termine funcionando bien.&lt;/p&gt;

&lt;p&gt;Me sirve pensar el componente igual que una &lt;a href="https://dev.to/silviutech/fastapi-cola-visible-para-correos-async-419m"&gt;cola visible para correos async&lt;/a&gt;: no todo cambio interno merece una reaccion dramatica en pantalla. A veces la mejor UI es la que reconoce que algo esta pasando, pero no interrumpe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un estado pendiente que no rompa el formulario
&lt;/h2&gt;

&lt;p&gt;Mi patron favorito es mantener una sola linea estable debajo del input y tratar el estado remoto como una progresion corta:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt;: ayuda neutral antes de cualquier comprobacion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pending&lt;/code&gt;: mensaje suave mientras corre la validacion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;warning&lt;/code&gt; o &lt;code&gt;success&lt;/code&gt;: resultado final si realmente aporta algo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;: solo para un problema concreto y accionable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La clave es que la region exista siempre. No aparece a empujones, no mueve el boton y no roba foco. Tampoco me gusta mostrar &lt;code&gt;success&lt;/code&gt; verde brillante en cada caso valido, porque termina haciendo tanto ruido como el error. Muchas veces basta con volver a una ayuda normal.&lt;/p&gt;

&lt;p&gt;Tambien intento retrasar un poco la comprobacion remota. Si validas en cada tecla, un valor incompleto como &lt;code&gt;tem email&lt;/code&gt; activa trabajo que todavia no tiene sentido. Un pequeno debounce o una comprobacion al salir del campo suele ser suficiente. En productos con mucho QA, este cambio hace que las &lt;a href="https://dev.to/hannahdev56/saas-pruebas-limpias-para-emails-de-upgrade-15kk"&gt;pruebas limpias para emails de upgrade&lt;/a&gt; sean bastante mas faciles de leer despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo de React con transiciones simples
&lt;/h2&gt;

&lt;p&gt;Este enfoque mantiene el helper text fijo y evita que el estado pendiente sacuda el layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useTransition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;HintState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_HINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HintState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usa un correo que puedas revisar hoy para terminar el registro.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksLikeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\S&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkEmailRisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/email-risk?email=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No se pudo revisar el correo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HintState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_HINT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTransition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;looksLikeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_HINT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Estamos comprobando este dominio...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nf"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkEmailRisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disposable&lt;/span&gt;
              &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Este correo podria expirar antes de recibir mensajes futuros.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_HINT&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setHint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos validar el dominio ahora mismo. Puedes continuar igual.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Correo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hintId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hint"&lt;/span&gt; &lt;span class="na"&gt;data-kind&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No es una solucion exotica, pero tiene tres ventajas practicas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce requests inutiles mientras la persona aun esta escribiendo&lt;/li&gt;
&lt;li&gt;mantiene el layout quieto casi todo el tiempo&lt;/li&gt;
&lt;li&gt;vuelve mucho mas legible la diferencia entre ayuda, espera y advertencia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si la politica real vive en backend, mejor todavia. El frontend acompana la decision sin fingir que controla todo. Eso deja el flujo mas honesto y bastante menos fragil.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS pequeno para evitar saltos y ruido
&lt;/h2&gt;

&lt;p&gt;Una parte importante de este patron ni siquiera esta en JavaScript. Esta en reservar espacio y bajar intensidad visual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.92rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5b6470&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"pending"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b5ccc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"warning"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#9a5a00&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hint&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese &lt;code&gt;min-height&lt;/code&gt; parece una tonteria, pero evita un montooon de micro saltos. Y cuando el mensaje pendiente entra en una linea reservada, la validacion se siente mucho mas rapida aunque la latencia real no cambie. Ese tipo de polish suele dar mejor resultado que meter otro spinner pequeñito por todos lados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que medir despues del release
&lt;/h2&gt;

&lt;p&gt;Yo revisaria al menos estas senales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;abandono del campo de email antes y despues del cambio&lt;/li&gt;
&lt;li&gt;tiempo hasta completar el formulario&lt;/li&gt;
&lt;li&gt;frecuencia de reintentos por el mismo valor&lt;/li&gt;
&lt;li&gt;cantidad de respuestas pendientes canceladas por nueva escritura&lt;/li&gt;
&lt;li&gt;sesiones con layout shift en el bloque del formulario&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si la comprobacion remota tarda bastante, incluso puedes registrar cuanto tiempo pasa el estado &lt;code&gt;pending&lt;/code&gt;. No para presumir precision, sino para saber cuando una interfaz necesita otra estrategia. A veces el mejor cambio no es optimizar la API; es dejar de molestar visualmente mientras esperas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debo bloquear el submit mientras corre el check async?
&lt;/h3&gt;

&lt;p&gt;Solo si la regla es critica para el negocio o la seguridad. En muchos casos basta con dejar continuar y resolver la politica final en servidor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conviene mostrar un spinner dentro del input?
&lt;/h3&gt;

&lt;p&gt;Puede funcionar, pero solo si no empuja iconos, padding o etiquetas. Si el spinner mueve el campo aunque sea un poco, normalmente sale mas caro de lo que ayuda.&lt;/p&gt;

&lt;h3&gt;
  
  
  Y si el dominio parece temporal pero el usuario quiere seguir?
&lt;/h3&gt;

&lt;p&gt;Prefiero una advertencia clara y no un castigo inmediato. Si el producto necesita correo duradero para recovery o billing, esa decision debe explicarse bien y confirmarse en backend.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>performance</category>
      <category>css</category>
    </item>
    <item>
      <title>React: evita CLS en validacion de email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:24:41 +0000</pubDate>
      <link>https://dev.to/silviutech/react-evita-cls-en-validacion-de-email-3hf3</link>
      <guid>https://dev.to/silviutech/react-evita-cls-en-validacion-de-email-3hf3</guid>
      <description>&lt;p&gt;Muchos formularios React validan el email solo cuando ya hay problema visible. El campo arranca limpio, luego aparece un error debajo, despues un hint distinto, luego un icono, y todo el bloque se mueve. Ese rebote visual parece pequeno, pero en signup y checkout se nota muchisimo. La persona esta intentando confirmar si escribio bien su correo, no perseguir mensajes que cambian de sitio.&lt;/p&gt;

&lt;p&gt;En equipos frontend esto suele pasar por una razon simple: se piensa mucho en la regex y poco en la estabilidad del estado. Cuando alguien pega algo como &lt;code&gt;dummy e mail&lt;/code&gt; o una direccion mal recordada tipo &lt;code&gt;temp gamil com&lt;/code&gt;, la interfaz deberia responder con contexto estable. Si el mensaje empuja el boton o desplaza el siguiente campo, la validacion termina sintiendose mas dramatica de lo necesario.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real no es la regex, es el salto visual
&lt;/h2&gt;

&lt;p&gt;Google describe &lt;a href="https://web.dev/articles/cls" rel="noopener noreferrer"&gt;Cumulative Layout Shift&lt;/a&gt; como una medida de movimientos inesperados en la pagina. Normalmente lo hablamos en imagenes o ads, pero el mismo principio afecta formularios pequenos: si el area de ayuda aparece tarde y mueve el layout, la tarea se vuelve menos comoda. No siempre vas a romper Core Web Vitals por un solo input, claro, pero si sumas varios campos nerviosos la experiencia queda medio fragil.&lt;/p&gt;

&lt;p&gt;Tambien hay un componente de usabilidad. Baymard viene observando desde hace anos que los formularios fallan mas por detalles de feedback que por reglas complejas; cuando el mensaje llega tarde o en el lugar equivocado, la correccion cuesta mas de lo que deberia &lt;a href="https://baymard.com/blog/checkout-usability" rel="noopener noreferrer"&gt;en sus investigaciones sobre checkout&lt;/a&gt;. Ese matiz importa bastante, por que el usuario no separa "accesibilidad", "CSS" y "performance" como nosotros. Solo siente que el form es claro, o no lo es.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patron de estado estable para email
&lt;/h2&gt;

&lt;p&gt;El patron que mejor me funciona tiene tres reglas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;la ayuda reserva espacio desde el primer render&lt;/li&gt;
&lt;li&gt;el mensaje cambia de contenido, no de posicion&lt;/li&gt;
&lt;li&gt;el error fuerte espera a una señal razonable, como &lt;code&gt;blur&lt;/code&gt; o submit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Esto hace que la validacion se sienta mas calmada. Primero orientas, luego confirmas, y solo despues corriges. Es la misma idea de &lt;code&gt;reusar estado cuando una accion se repite&lt;/code&gt;: si una zona ya existe en la interfaz, reusarla suele ser mejor que insertar otra a ultima hora. Y en flujos de email con varias comprobaciones, esa continuidad se vuelve aun mas util, igual que en estas &lt;a href="https://dev.to/silviutech/react-evita-dobles-envios-al-reenviar-email-56m5"&gt;aprobaciones por email con menos ruido&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Si ademas tu producto deja probar correos temporales para demos o QA, conviene explicar el criterio en texto humano. Un mensaje breve puede decir que aceptas dominios comunes y tambien algunos de prueba, en vez de castigar sin contexto. En ciertos equipos eso evita tickets medio bobos cuando alguien usa un inbox temporal de &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; para una smoke test o una demo interna.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo en React y CSS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksLikeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\S&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;touched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTouched&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;touched&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;looksLikeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;showError&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa el formato. Usa algo como nombre@dominio.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Te enviaremos confirmaciones y cambios importantes a este correo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"emailField"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"emailLabel"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onBlur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTouched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emailMessage error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emailMessage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.emailField&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.emailLabel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.emailMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5b6472&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.emailMessage.error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo importante no es la regex, sinceramente. Lo importante es que &lt;code&gt;min-height&lt;/code&gt; reserva el espacio, &lt;code&gt;aria-describedby&lt;/code&gt; mantiene la relacion semantica y &lt;code&gt;aria-live="polite"&lt;/code&gt; anuncia el cambio sin gritar. En formularios de verdad, esta combinacion evita un monton de micro sacudidas que despues nadie quiere depurar. Es una mejora pequena, pero pega.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde se nota la mejora en accesibilidad y conversion
&lt;/h2&gt;

&lt;p&gt;La mejora accesible se ve rapido:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la instruccion no desaparece cuando la persona empieza a escribir&lt;/li&gt;
&lt;li&gt;el lector de pantalla encuentra siempre la misma zona de ayuda&lt;/li&gt;
&lt;li&gt;el error explica que hacer, no solo que algo fallo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La mejora de producto tambien aparece. Cuando el campo conserva su altura, el boton no baja, el ojo no pierde referencia y el formulario parece mas rapido aunque el JavaScript sea el mismo. Ese tipo de polish suele entrar en la categoria de "detalle menor", pero luego mueve completion rate, sobre todo en movil. No tengo una formula magica aca, pero si he visto que los forms con mensajes estables reciben menos correcciones torpes y menos abandonos raros.&lt;/p&gt;

&lt;p&gt;Yo revisaria dos escenarios extra, por si acaso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zoom alto en iOS Safari&lt;/li&gt;
&lt;li&gt;autocompletado agresivo en Chrome con email guardado&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si en esos casos la ayuda sigue clara y el layout no tiembla, vas por buen camino. Si no, arreglarlo temprano cuesta poco; arreglarlo despues, cuando ya se mezclo con analytics y copys urgentes, es mucho mas molesto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rapidas
&lt;/h2&gt;

&lt;p&gt;Q: ¿Debo validar en cada tecla?&lt;br&gt;&lt;br&gt;
A: Solo si el feedback no interrumpe. Muchas veces &lt;code&gt;blur&lt;/code&gt; da una experiencia mas limpia, aunque depende del caso.&lt;/p&gt;

&lt;p&gt;Q: ¿Necesito &lt;code&gt;aria-live&lt;/code&gt; siempre?&lt;br&gt;&lt;br&gt;
A: No siempre, pero en mensajes que cambian sin mover foco suele ayudar bastante.&lt;/p&gt;

&lt;p&gt;Q: ¿Esto sirve si luego hago validacion remota?&lt;br&gt;&lt;br&gt;
A: Si. La capa local ordena la experiencia primero, y la remota entra despues sin meter mas caos.&lt;/p&gt;

&lt;p&gt;En React, mejorar un campo de email no exige otra libreria ni un rediseño completo. Exige pensar el estado visual como parte del rendimiento percibido. Cuando el mensaje no mueve nada y la instruccion sigue presente, la validacion deja de sentirse tosca. Y eso, aunque sea sutil, los usuarios lo notan un monton.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>a11y</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
