DEV Community

Silviu Technology
Silviu Technology

Posted on

React: estados async sin saltos visuales

React: estados async sin saltos visuales

Un formulario que valida un correo en segundo plano puede sentirse lento aunque la API responda rápido. El motivo suele estar en la interfaz: el botón cambia de tamaño, aparece un mensaje debajo y todo el formulario baja unos píxeles. Ese movimiento parece pequeño, pero rompe la atención y complica el uso con teclado o lector de pantalla.

En mis pruebas de frontend con React he encontrado más útil diseñar la espera como un estado completo, no como un spinner pegado al final. Esto aplica igual cuando comprobamos una throwaway email address, un dominio o cualquier regla de registro. La meta es que el usuario siempre sepa qué ocurre y que el layout siga siendo predecible.

El problema no es solo esperar

Una validación async tiene al menos cuatro momentos: vacío, comprobando, aceptado y rechazado. Si React solo renderiza el mensaje cuando llega la respuesta, reserva cero espacio durante los primeros tres estados. El resultado es un salto visual y, a veces, un foco que termina en un lugar raro.

También hay una trampa de rendimiento. Si cada pulsación dispara una petición, la respuesta vieja puede ganar a la nueva. El correo actual queda marcado como inválido aunque el usuario ya lo corrigió. No es un bug muy visible, pero ocurre bastante en redes lentas.

Un estado visual estable

Primero separo el estado de la petición del contenido que se muestra. El mensaje ocupa una línea incluso cuando está vacío, y el botón mantiene su ancho:

import { useState } from "react";

export function EmailField({ checkEmail }) {
  const [value, setValue] = useState("");
  const [status, setStatus] = useState("idle");
  const [message, setMessage] = useState("");

  async function handleBlur() {
    if (!value.trim()) return;
    setStatus("checking");
    setMessage("Comprobando dirección…");

    const result = await checkEmail(value);
    setStatus(result.ok ? "valid" : "invalid");
    setMessage(result.message);
  }

  return (
    <div className="field">
      <label htmlFor="email">Correo electrónico</label>
      <input
        id="email"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        onBlur={handleBlur}
        aria-describedby="email-help"
        aria-invalid={status === "invalid"}
      />
      <p id="email-help" className="field-message" aria-live="polite">
        {message}
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

La clase field-message debe tener una altura mínima, no depender de que el texto exista:

.field-message {
  min-height: 1.5rem;
  margin: .35rem 0 0;
  color: #59636e;
}

.field-message:empty {
  visibility: hidden;
}

input:focus-visible {
  outline: 3px solid #2457d6;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

El espacio reservado reduce el layout shift. Además, aria-live="polite" anuncia el resultado sin interrumpir al usuario. Para un error que bloquea el envío, conviene sumar un resumen de errores y mover el foco con cuidado, pero no en cada cambio de tecla.

Evitar respuestas fuera de orden

Para una búsqueda o validación frecuente, uso un contador de solicitud o AbortController. La idea es sencilla: solo la última petición puede modificar el estado. Un debounce de 250–350 ms tambien ayuda a no saturar el endpoint, sobre todo en móviles.

let requestId = 0;

async function validateLatest(email, checkEmail, update) {
  const id = ++requestId;
  update("checking");
  const result = await checkEmail(email);
  if (id !== requestId) return;
  update(result.ok ? "valid" : "invalid");
}
Enter fullscreen mode Exit fullscreen mode

No presentaría dummy e mail como una dirección real en métricas de producto: si aparece en documentación, dejaria claro que es un dato de prueba. Lo mismo con tamp mail com, que puede acabar confundiendo un filtro o una revisión manual.

Accesibilidad y rendimiento en conjunto

El estado de carga necesita contraste suficiente, texto comprensible y un destino de foco estable. Un spinner solo visual no comunica nada a un lector de pantalla. Si se usa, acompañalo con texto como “Comprobando dirección”.

Para comparar decisiones de diseño, puedes revisar una guía sobre validación async sin saltos visuales y ver cómo los esqueletos que respetan la accesibilidad mantienen una estructura estable.

En pruebas de registro, una dirección temporal puede ser útil para aislar el flujo, por ejemplo con un servicio para temp mail so o para get temporary email, pero no debe convertirse en una recomendación de seguridad. La interfaz debe explicar qué se verificó, no prometer que una bandeja existe para siempre.

Checklist final

  • ¿El mensaje tiene espacio reservado antes de llegar?
  • ¿El botón conserva tamaño y etiqueta durante la espera?
  • ¿El resultado se anuncia con aria-live sin robar el foco?
  • ¿Las respuestas antiguas quedan descartadas?
  • ¿El debounce evita peticiones por cada tecla?
  • ¿El estado inválido tiene texto y contraste claros?
  • ¿La prueba incluye teclado, móvil y una red lenta?

Una buena validación async no se nota como una animación espectacular. Se nota porque el formulario mantiene su sitio, responde con claridad y no obliga a adivinar. Ese pequeño cuidado suele mejorar a la vez accesibilidad, rendimiento percibido y confianza en el producto.

Top comments (0)