05. Frontend Reactivo: UX Minimalista y Sincronización Inteligente de Carritos
Tener una API rápida y segura en la nube no sirve de nada si tu interfaz de usuario se siente tosca, lenta o confusa. En el e-commerce moderno, el navegador del cliente es el verdadero campo de batalla. Un retraso visual al presionar "Añadir al carrito", una página que parpadea en blanco al recargar o perder los artículos acumulados por no haber iniciado sesión a tiempo son problemas silenciosos que destruyen las tasas de conversión.
En esta última entrega de nuestra bitácora, saltaremos al frontend de DevMart para ver cómo configuramos el nuevo motor de diseño de Tailwind CSS v4 y cómo implementamos en React 19 un manejo de estado transparente con sincronización asíncrona de carritos e inicio de sesión fluido.
🎨 Tokenización de Marca con Tailwind CSS v4
Con el lanzamiento de Tailwind CSS v4, el archivo de configuración JS desaparece en favor de un enfoque puramente declarativo en el archivo CSS principal mediante la directiva @theme. Esto nos permite definir nuestra paleta de colores de marca y tipografía directamente en el archivo base:
/* src/index.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@import "tailwindcss";
@theme {
--color-brand-50: #E6F0FF;
--color-brand-100: #B8D4FF;
--color-brand-500: #0066FF; /* Color corporativo principal de DevMart */
--color-brand-900: #001D47;
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
}
¿Por qué este diseño?
- Identidad de Marca Sólida: Al definir tokens CSS estándar (
--color-brand-500), evitamos el uso de colores genéricos (comobg-blue-600). Toda la web respira la paleta de color azul y gris minimalista de DevMart de forma consistente. - Rendimiento superior: Al compilarse nativamente con Rust en Vite, Tailwind v4 genera un archivo CSS minificado minúsculo y optimizado, acelerando el primer renderizado en dispositivos móviles con redes lentas (3G/4G).
⚡ React Router v8 y Enrutamiento Single Page (SPA)
Para garantizar transiciones instantáneas sin recargas totales del navegador, implementamos un enrutamiento del lado del cliente. Sin embargo, esto introduce un reto técnico cuando desplegamos en producción en servidores de archivos estáticos como Vercel: el error 404 al recargar.
El Problema del SPA Routing:
Si el usuario está navegando y decide recargar directamente la ruta https://devmart-e.vercel.app/login, Vercel busca físicamente un archivo llamado /login/index.html en el servidor, no lo encuentra y devuelve un error 404.
La Solución (vercel.json):
Configuramos una regla de reescritura de URLs para que cualquier ruta solicitada apunte siempre a nuestro punto de entrada index.html, dejando que React Router v8 tome el control de la ruta una vez cargado el código JS:
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
💾 Estado Global: Zero Dependencias y Sincronización Automática
En lugar de sobrecargar el frontend instalando librerías pesadas de gestión de estado global como Redux, en DevMart empleamos la API nativa de React Context combinada con patrones inteligentes de persistencia.
graph TD
Guest[Navegación Anónima] -->|Agrega productos| LocalStorage[(LocalStorage)]
Guest -->|Click en Login| Auth[AuthContext]
Auth -->|Inicio de Sesión Exitoso| Merge[CartContext: mergeCartService]
Merge -->|Envía payload guest al backend| Backend[Express API]
Backend -->|Valida stock y fusiona| DB[(PostgreSQL)]
Merge -->|Limpia local| LocalStorage
Merge -->|Re-fetch del carrito DB actualizado| State[React State: cartDetails]
1. Decodificación Nativa de JWT sin Librerías
Para mantener el bundle del cliente liviano, evitamos instalar decodificadores de tokens externos. En su lugar, implementamos una simple decodificación nativa utilizando la función atob del navegador para leer el payload del JWT:
// src/context/AuthContext.jsx
const decodeToken = (token) => {
try {
const base64Payload = token.split('.')[1];
return JSON.parse(atob(base64Payload));
} catch {
return null;
}
};
2. Auto-Renovación de Tokens en Caliente (fetchWithRefresh)
Para evitar que un usuario sea desconectado abruptamente a la mitad de su proceso de compra debido a que su token JWT expiró, implementamos un interceptor en la capa de servicios (cart.service.js):
// src/services/cart.service.js
const fetchWithRefresh = async (url, options) => {
let response = await fetch(url, options);
// Si el servidor nos dice que no estamos autorizados (Token vencido)
if (response.status === 401) {
const currentToken = localStorage.getItem('auth_token');
try {
// Solicitamos un token fresco en caliente
const newToken = await refreshTokenService(currentToken);
localStorage.setItem('auth_token', newToken);
// Reintentamos la petición original con el nuevo token sin interrumpir al usuario
const retryOptions = {
...options,
headers: { ...options.headers, 'Authorization': `Bearer ${newToken}` }
};
response = await fetch(url, retryOptions);
} catch {
// Si la renovación falla, cerramos sesión limpiamente
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
throw new Error('Sesión expirada. Inicie sesión nuevamente.');
}
}
return response;
};
💡 Impacto en el negocio: Esto crea una UX invisible y robusta. El cliente nunca ve una pantalla de error por culpa de un token expirado mientras está pagando.
🛒 Fusión de Carritos y Control de Stock en el Cliente
Cuando un usuario inicia sesión, el CartProvider detecta la transición de estado de invitado a autenticado mediante una referencia (prevAuthRef = useRef(isAuthenticated)). Si existían productos en el almacenamiento local del visitante anónimo, ejecuta un protocolo de fusión:
- Envía los datos al backend: Envía los identificadores y las cantidades al endpoint
/cart/merge. - Validación de Stock Segura: El backend valida que los productos sigan existiendo en el catálogo y que no superemos el stock disponible en inventario antes de agregarlos al carrito de la base de datos.
- Limpieza y Carga: Tras la fusión exitosa, el frontend limpia el
localStoragepara evitar dobles fusiones a futuro y hace un re-fetch automático del carrito actualizado.
Actualizaciones Optimistas para mayor fluidez
Al eliminar un artículo del carrito, no hacemos esperar al usuario hasta que el servidor de base de datos responda. En su lugar, aplicamos una actualización optimista: eliminamos el componente visual de la pantalla inmediatamente de forma animada. Si por algún motivo la API de red falla, hacemos un rollback restaurando el estado previo en silencio:
// src/context/CartContext.jsx
const removeCartItem = async (cartDetailId) => {
if (isAuthenticated) {
const previousDetails = cartDetails;
// 🚀 Eliminación optimista inmediata en pantalla
setCartDetails((prev) => prev.filter((item) => item.id !== cartDetailId));
try {
await removeFromCart(cartDetailId);
} catch (error) {
// 🔄 Rollback si falla la red
setCartDetails(previousDetails);
throw error;
}
}
// ...
};
🏁 Conclusión: El valor del Criterio Técnico
A lo largo de estos 5 posts, hemos recorrido el diseño de DevMart desde la visión de arquitectura y base de datos, pasando por integraciones de IA (Gemini) en chatbots, hasta llegar a la interfaz de usuario en React.
El desarrollo de software profesional no consiste en usar la tecnología más compleja o costosa; consiste en saber elegir el balance perfecto entre velocidad de desarrollo, seguridad para el usuario y costo operativo para el negocio.
Con una arquitectura serverless a costo cero, una experiencia móvil integrada en Telegram potenciada por Gemini NLP y una Single Page Application en React 19 optimizada, DevMart está listo para competir en el mercado de comercio electrónico con la máxima eficiencia de costos.
📅 Publicado en la Bitácora de Desarrollo de DevMart. Ver Índice General