Resolviendo Regresiones Críticas en Tu Tienda Web: Del E2E a la UI
En el dinámico mundo del desarrollo de e-commerce, cada nueva característica trae consigo la posibilidad de introducir regresiones inesperadas. Recientemente, en el proyecto 'Tu Tienda Web', nos enfrentamos a un conjunto de desafíos críticos que impactaban desde la estabilidad de nuestros tests E2E hasta la integridad de los datos del usuario y la experiencia de usuario en el carrito de compras. Este post detalla cómo abordamos y resolvimos estas regresiones multifacéticas, asegurando una experiencia de usuario robusta y confiable.
Problema 1: La Persistencia de Datos en la Configuración (y sus trampas)
Uno de los desafíos más sutiles surgió en la gestión de la configuración de usuario. Múltiples instancias de nuestro hook useProfile compartían el mismo estado, y una actualización asíncrona de un perfil (por ejemplo, setProfile) podía disparar un reset prematuro del formulario. Esto llevaba a una sobrescritura de las ediciones en curso con valores antiguos, a pesar de que el usuario recibía un mensaje de "guardado con éxito". La raíz del problema era un reset incondicional del formulario.
Para resolverlo, implementamos una lógica para que el formulario solo se "resetease" o sincronizase si los datos del perfil habían cambiado realmente. Esto se logró mediante una comparación profunda del contenido del perfil actual con una referencia al último perfil guardado sincronizado (lastSyncedFormDataRef), evitando así la pérdida de datos y las condiciones de carrera.
import { useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
interface UserProfile {
name: string;
email: string;
// ... otros campos
}
function useProfileForm(initialProfile: UserProfile) {
const [profile, setProfile] = useState<UserProfile>(initialProfile);
const lastSyncedProfileRef = useRef<UserProfile>(initialProfile);
const handleSave = async () => {
// Simulación de llamada API asíncrona
await new Promise(resolve => setTimeout(resolve, 500));
// CRÍTICO: Solo resetear si los datos realmente cambiaron
if (!isEqual(profile, lastSyncedProfileRef.current)) {
lastSyncedProfileRef.current = profile;
// En un formulario real, aquí se llamaría a 'reset(profile)' de la librería de forms
console.log('Perfil actualizado y estado sincronizado.');
} else {
console.log('No hay cambios significativos para sincronizar.');
}
};
useEffect(() => {
setProfile(initialProfile);
lastSyncedProfileRef.current = initialProfile;
}, [initialProfile]);
return { profile, setProfile, handleSave };
}
Este useProfileForm ilustra cómo la comparación de contenido asegura que las acciones de sincronización y reseteo solo ocurran cuando hay cambios genuinos, previniendo la pérdida de información en formularios complejos.
Problema 2: El Misterio del removeChild y la Revalidación de Rutas
Otra regresión, particularmente frustrante, fue un error Cannot read properties of null (reading 'removeChild') que aparecía en /dashboard/settings/general. Este error se manifestaba durante las transiciones de guardado y estaba relacionado con una revalidación de ruta demasiado agresiva. Inicialmente, se intentó revalidar la ruta completa de /dashboard/settings, lo que provocaba un refresco de todo el subárbol de componentes de configuración mientras una animación estaba en curso, llevando a que React intentara manipular un nodo DOM que ya no existía.
La solución consistió en revalidar únicamente la ruta pública del catálogo (/${storeId}) en lugar de la ruta de edición. Además, se "endureció" el DOM utilizando AnimatePresence de framer-motion alrededor de los componentes motion.div condicionales. Esto asegura que los componentes se monten y desmonten correctamente con una key única, permitiendo que las animaciones de salida se completen antes de que el nodo sea completamente eliminado del DOM.
import { AnimatePresence, motion } from 'framer-motion';
import React, { useState } from 'react';
interface SettingPanelProps {
id: string;
children: React.ReactNode;
isActive: boolean;
}
const SettingPanel: React.FC<SettingPanelProps> = ({ id, children, isActive }) => {
return (
<AnimatePresence mode="wait">
{isActive && (
<motion.div
key={id} // CRÍTICO: `key` para transiciones correctas y estabilidad del DOM
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
// Ejemplo de uso
function SettingsContainer() {
const [activeTab, setActiveTab] = useState('general');
return (
<div>
<nav>
<button onClick={() => setActiveTab('general')}>General</button>
<button onClick={() => setActiveTab('billing')}>Facturación</button>
</nav>
<SettingPanel id="general-settings" isActive={activeTab === 'general'}>
<div>Panel de configuración general...</div>
</SettingPanel>
<SettingPanel id="billing-settings" isActive={activeTab === 'billing'}>
<div>Panel de facturación...</div>
</SettingPanel>
</div>
);
}
Al envolver los paneles de configuración con AnimatePresence y asignar una key única, garantizamos que React maneje correctamente el ciclo de vida del componente durante las transiciones, eliminando el error removeChild.
Problema 3: El Carrito Indomable: Desbordamientos en Diseños Responsivos
Finalmente, se identificó un problema de diseño en el carrito de compras para vistas de tablet y escritorio (>=768px). Debido a que el componente Dialog utilizaba un diseño grid con min-width:auto, los nombres de productos excesivamente largos desbordaban el modal, arruinando la interfaz de usuario.
La solución fue un ajuste sencillo pero efectivo de las clases CSS en el componente Cart.tsx. Aplicamos w-full min-w-0 overflow-hidden al contenedor del nombre del producto. Estas clases de Tailwind CSS garantizan que el elemento ocupe todo el ancho disponible, tenga un ancho mínimo de cero para permitir que se "encoga" si es necesario, y que cualquier contenido que exceda su límite sea recortado, evitando el desbordamiento.
import React from 'react';
interface CartItemProps {
productName: string;
quantity: number;
price: number;
}
const CartItem: React.FC<CartItemProps> = ({ productName, quantity, price }) => {
return (
<div className="flex items-center space-x-4 p-2 border-b">
<div className="flex-grow">
{/* CRÍTICO: Clases para evitar desbordamiento en nombres largos */}
<p className="text-sm font-medium w-full min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{productName}
</p>
<p className="text-xs text-gray-500">Cantidad: {quantity}</p>
</div>
<p className="text-sm font-semibold">${price.toFixed(2)}</p>
</div>
);
};
function CartDisplay() {
const items = [
{ productName: 'Producto con un nombre muy muy largo que causa desbordamiento de la interfaz', quantity: 1, price: 29.99 },
{ productName: 'Producto Corto', quantity: 2, price: 10.00 },
];
return (
<div className="border rounded-lg shadow-lg max-w-md mx-auto p-4">
<h3 className="text-lg font-bold mb-4">Tu Carrito</h3>
{items.map((item, index) => (
<CartItem key={index} {...item} />
))}
<div className="text-right mt-4 font-bold">
Total: ${items.reduce((acc, item) => acc + item.quantity * item.price, 0).toFixed(2)}
</div>
</div>
);
}
Con estas clases, el nombre del producto se recorta elegantemente con puntos suspensivos si es demasiado largo, manteniendo el diseño del carrito intacto.
Conclusión
Abordar estas regresiones en 'Tu Tienda Web' subraya la importancia de una ingeniería cuidadosa. Desde la implementación de lógica de comparación de contenido para asegurar la integridad de los datos en formularios, hasta el manejo robusto del DOM durante las transiciones y la aplicación de estilos responsivos, cada corrección contribuyó a una aplicación más estable y una mejor experiencia de usuario. La inversión en pruebas E2E robustas fue crucial para identificar estas regresiones, permitiendo una acción correctiva rápida.
Próximos Pasos
Continuaremos fortaleciendo nuestra suite de pruebas E2E para cubrir más escenarios de interacción complejos y transiciones de estado. Además, se explorarán más a fondo las capacidades de gestión de estado global y la inmutabilidad de datos en aplicaciones React para prevenir futuras condiciones de carrera y asegurar la consistencia del estado en entornos de alto rendimiento.
Generated with Gitvlg.com