30 Reglas de UX en React
Reglas para construir interfaces React que se sientan rápidas, tolerantes e intuitivas. Cubre estados de carga, recuperación de errores, retroalimentación, accesibilidad y diseño de interacción.
Rendimiento Percibido (Reglas 1-10)
1. Muestra algo inmediatamente. Nunca muestres una pantalla en blanco. Usa cargadores esqueléticos, contenido marcador de posición, o datos antiguos en caché mientras se cargan datos frescos.
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={id} />
</Suspense>2. Usa actualizaciones optimistas para acciones iniciadas por el usuario. Cuando un usuario hace clic en "me gusta", "guardar" o "eliminar", actualiza la UI instantáneamente. Revierte si el servidor rechaza. Esperar al servidor hace que tu aplicación se sienta lenta.
const [optimisticLikes, addLike] = useOptimistic(
likes,
(state, newLike: Like) => [...state, newLike]
);3. Muestra estados pendientes en botones y formularios. Desactiva el botón de envío y muestra un spinner durante el envío. Los usuarios nunca deberían preguntarse si su clic se registró.
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending} type="submit">
{pending ? "Guardando..." : "Guardar"}
</button>
);
}4. Usa transiciones para actualizaciones no urgentes. Envuelve actualizaciones de estado costosas en useTransition para mantener la UI receptiva. El contenido anterior permanece interactivo mientras se renderiza el contenido nuevo.
const [isPending, startTransition] = useTransition();
function handleSearch(query: string) {
startTransition(() => {
setSearchResults(filterLargeList(query));
});
}5. Pre-carga rutas que el usuario es probable que visite. Next.js Link pre-carga por defecto en hover. Para navegación programática, usa router.prefetch("/target").
6. Transmite contenido progresivamente. No esperes a que todos los datos estén disponibles antes de mostrar algo. Usa múltiples límites de Suspense para que cada sección aparezca tan pronto como sus datos estén listos.
7. Debounce en entradas de búsqueda. Escribir dispara en cada pulsación de tecla. Usa debounce con 300ms de retraso para evitar solicitudes excesivas y cambios en la UI.
8. Evita cambios de diseño. Reserva espacio para imágenes (establece dimensiones), fuentes (usa next/font), y contenido dinámico (establece min-height). Los usuarios nunca deberían perder su posición de desplazamiento porque el contenido se movió.
9. Cachea agresivamente, invalida con precisión. Usa SWR, TanStack Query, o caché de Next.js para mostrar datos antiguos instantáneamente mientras se revalida en segundo plano. Los usuarios ven contenido inmediatamente.
10. Proporciona retroalimentación instantánea para cada interacción. Estados hover en botones, estados activos en enlaces, anillos de enfoque en entradas, destacados de selección en alternancias. Cada clic y hover debería producir retroalimentación visible.
Manejo de Errores y Recuperación (Reglas 11-18)
11. Muestra errores en línea, no cajas de alerta. Muestra errores de validación junto al campo relevante. Nunca uses alert() o toast genérico para validación de formularios.
<div>
<label htmlFor="email">Email</label>
<input id="email" aria-invalid={!!errors.email} aria-describedby="email-error" />
{errors.email && (
<p id="email-error" className="text-sm text-red-600">{errors.email}</p>
)}
</div>12. Haz que los errores sean recuperables. Cada estado de error debería incluir una forma de avanzar: un botón de reintento, un enlace para volver, o una sugerencia de qué probar a continuación.
13. Preserva la entrada del usuario en errores. Si el envío del formulario falla, nunca borres el formulario. Mantén todos los datos ingresados y resalta solo los campos que necesitan corrección.
14. Usa notificaciones toast para operaciones en segundo plano. Las confirmaciones de guardado, finalizaciones de tareas asincrónicas, y errores sin bloqueo pertenecen a los toasts. Mantenlos breves (menos de 5 segundos) con una acción cuando sea relevante.
15. Maneja estados vacíos cuidadosamente. Una lista vacía no es un error. Muestra un mensaje útil con un llamado a la acción.
{items.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">Sin proyectos todavía</p>
<Button onClick={onCreate}>Crea tu primer proyecto</Button>
</div>
) : (
<ProjectList items={items} />
)}16. Muestra confirmación para acciones destructivas. Eliminar, cancelar suscripción, remover miembro del equipo. Estas necesitan un diálogo de confirmación con consecuencias claras establecidas.
17. Proporciona deshacer en lugar de confirmación cuando sea posible. "Mensaje eliminado. Deshacer" es mejor UX que "¿Estás seguro de que quieres eliminar?" El usuario puede proceder más rápido, y los errores son recuperables.
18. Maneja el estado sin conexión correctamente. Detecta estado sin conexión con navigator.onLine y los eventos online/offline. Muestra un banner, colas de mutaciones, y sincroniza cuando se reconecta.
Accesibilidad (Reglas 19-25)
19. Usa HTML semántico primero. button para acciones, a para navegación, nav para navegación, main para contenido principal, h1-h6 en orden. El HTML semántico es accesible por defecto.
20. Todo elemento interactivo debe ser accesible por teclado. El orden de tabulación debería ser lógico. Enter/Space activan botones. Escape cierra modales. Teclas de flecha navegan menús. Prueba sin un ratón.
21. Toda imagen necesita un atributo alt. Alt descriptivo para imágenes de contenido. alt="" vacío para imágenes decorativas. Nunca omitas el atributo.
| Tipo de Imagen | Texto Alt |
|---|---|
| Contenido (foto, gráfico) | Describe qué muestra la imagen |
| Decorativa (fondo, divisor) | alt="" (cadena vacía) |
| Funcional (botón de icono) | Describe la acción: "Cerrar menú" |
22. El color solo no debe transmitir significado. Los estados de error necesitan iconos o texto además del color rojo. Los estados de éxito necesitan más que verde. Considera usuarios daltónicos (8% de los hombres).
23. Maneja el enfoque en cambios de ruta y modales. Cuando un modal se abre, enfoca el primer elemento enfocable dentro. Cuando se cierra, devuelve el enfoque al disparador. En navegación del lado del cliente, enfoca el contenido principal o el encabezado.
24. Anuncia cambios de contenido dinámico. Usa aria-live="polite" para actualizaciones de estado (notificaciones de toast, conteos de resultados de búsqueda) para que los lectores de pantalla los anuncien.
<div aria-live="polite" aria-atomic="true">
{results.length} resultados encontrados
</div>25. Etiqueta todos los controles de formulario. Cada entrada necesita una label visible o aria-label. El texto de marcador de posición no es una etiqueta. Asocia etiquetas con htmlFor coincidiendo con el id de entrada.
Diseño de Interacción (Reglas 26-30)
26. Haz las áreas clicables lo suficientemente grandes. Los objetivos táctiles deberían ser al menos 44x44px (WCAG). Botones y enlaces pequeños frustran a los usuarios móviles.
// Bien: botón con padding
<button className="px-4 py-3 min-h-[44px]">Guardar</button>
// Mal: objetivo diminuto
<button className="px-1 py-0.5 text-xs">x</button>27. Muestra estados de carga en contexto. Un spinner dentro del botón que se hizo clic es mejor que una pantalla de carga de página completa. Muestra progreso donde el usuario está mirando.
28. Usa divulgación progresiva. No abrumarás a los usuarios con cada opción a la vez. Muestra lo esencial primero, revela opciones avanzadas bajo demanda (acordeón, "Mostrar más", pestañas).
<div>
<BasicSettings />
<details>
<summary className="cursor-pointer text-sm text-blue-600">
Configuración avanzada
</summary>
<AdvancedSettings />
</details>
</div>29. Respeta las preferencias del usuario. Honra prefers-reduced-motion para animaciones, prefers-color-scheme para modo oscuro, y prefers-contrast para alto contraste. Estas son configuraciones de accesibilidad a nivel del sistema.
// CSS
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}// React hook
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");30. Sé consistente. Las mismas acciones deberían verse igual en todas partes. Si "Guardar" es un botón azul en una página, debería ser un botón azul en cada página. Ubicación consistente, estilo consistente, comportamiento consistente. La consistencia reduce la carga cognitiva.
FAQs
¿Cuál es la diferencia entre useOptimistic y useTransition para UX?
useOptimisticmuestra un resultado inmediato, asumido como exitoso mientras se ejecuta una Server Action (revierte en caso de fallo)useTransitionmantiene la UI actual interactiva mientras se renderiza una actualización de estado no urgente en segundo plano- Usa
useOptimisticpara mutaciones (like, save); usauseTransitionpara filtrado costoso o navegación
¿Cómo muestras un estado pendiente en un botón de envío de formulario?
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending} type="submit">
{pending ? "Guardando..." : "Guardar"}
</button>
);
}useFormStatus lee el estado pendiente del <form> padre más cercano.
¿Por qué deberías usar errores en línea en lugar de cajas de alerta para validación de formularios?
- Los errores en línea aparecen junto al campo que necesita corrección, reduciendo la carga cognitiva
alert()bloquea el hilo y no proporciona contexto sobre qué campo falló- Los errores en línea funcionan con
aria-describedbypara accesibilidad de lector de pantalla
Trampa: ¿Qué sucede si borras entradas de formulario en un envío fallido?
- Los usuarios pierden todos sus datos ingresados y deben escribir todo nuevamente
- Esto crea frustración y aumenta las tasas de abandono
- Siempre preserva la entrada del usuario en errores y resalta solo los campos que necesitan corrección
¿Cuándo deberías usar deshacer en lugar de un diálogo de confirmación?
- Deshacer es mejor UX para acciones reversibles (eliminar mensaje, archivar elemento, remover etiqueta)
- Los diálogos de confirmación ralentizan a los usuarios y a menudo se descartan sin leer
- Reserva diálogos de confirmación para acciones verdaderamente destructivas e irreversibles (eliminar cuenta, cancelar suscripción)
¿Cuál es el tamaño mínimo del objetivo táctil y por qué importa?
- WCAG recomienda al menos 44x44 píxeles para elementos interactivos
- Los objetivos pequeños causan toques incorrectos en dispositivos móviles, frustrando a los usuarios
- Usa padding adecuado (
px-4 py-3 min-h-[44px]) incluso en botones pequeños
¿Cómo anuncias cambios de contenido dinámico a lectores de pantalla?
<div aria-live="polite" aria-atomic="true">
{results.length} resultados encontrados
</div>Usa aria-live="polite" para actualizaciones no urgentes (conteos de búsqueda, toasts) para que el lector de pantalla los anuncie después del habla actual.
¿Cómo deberías manejar la gestión del enfoque cuando un modal se abre y se cierra?
- Cuando modal se abre: mueve el enfoque al primer elemento enfocable dentro del modal
- Atrapa el enfoque dentro del modal (Tab debería ciclar solo a través de elementos del modal)
- Cuando modal se cierra: devuelve el enfoque al elemento que disparó el modal
Trampa: ¿Por qué el texto de marcador de posición no es un sustituto para una etiqueta?
- El texto de marcador de posición desaparece tan pronto como el usuario comienza a escribir, eliminando el contexto
- Los lectores de pantalla pueden no anunciar de manera confiable el texto de marcador de posición como una etiqueta
- Siempre usa una
<label>visible conhtmlForoaria-labelen la entrada
¿Cómo escribes el hook useMediaQuery para respetar preferencias del usuario en TypeScript?
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(false);
useEffect(() => {
const mql = window.matchMedia(query);
setMatches(mql.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}¿Cómo escribes los props para un componente fallback de boundary de error en TypeScript?
type ErrorFallbackProps = {
error: Error;
resetErrorBoundary: () => void;
};
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div role="alert">
<p>Algo salió mal: {error.message}</p>
<button onClick={resetErrorBoundary}>Intentar de nuevo</button>
</div>
);
}¿Qué consultas de medios CSS deberías respetar para accesibilidad?
prefers-reduced-motion: reduce-- desactiva o minimiza animacionesprefers-color-scheme: dark-- ofrece modo oscuroprefers-contrast: more-- aumenta contraste para usuarios de baja visión- Estas son configuraciones a nivel del sistema en las que los usuarios dependen
Referencia Rápida
| Categoría | Regla Clave |
|---|---|
| Velocidad | Muestra algo inmediatamente. Actualizaciones optimistas. Transmite progresivamente. |
| Errores | Errores en línea. Preserva entrada. Siempre ofrece recuperación. |
| Accesibilidad | HTML semántico. Accesible por teclado. Gestión del enfoque. |
| Retroalimentación | Estados pendientes en botones. Toast para operaciones en segundo plano. Deshacer sobre confirmar. |
| Toque | Objetivos mínimos de 44px. Carga consciente del contexto. Divulgación progresiva. |