Volver al inicio

React Modal Stack: Arquitectura para IT

El artículo describe la arquitectura de una pila modal tipada para aplicaciones React. Resuelve problemas de duplicación del DOM, prop drilling y dependencias mediante slice de Redux, hooks de superposición y envoltorios de fuente. Proporciona ejemplos de código y estrategias de escalado.

Pila de Modales Tipada en React para UI Compleja
Advertisement 728x90

Arquitectura de pila de modales en React para apps escalables

En aplicaciones React complejas, los modales se convierten rápidamente en un dolor de cabeza. Un modal simple con useState funciona bien de forma aislada, pero a medida que el proyecto crece, terminas con componentes duplicados, perforación de props y dependencias de estado enredadas. ¿La solución? Una pila de superposiciones tipada que separa la lógica por fuente de activación.

Imagina un panel de administración típico de un marketplace con una tabla de productos. Cada fila necesita un modal para agregar identificadores (IMEI, SGTIN), completo con tablas de pedidos, validación, cargas de Excel y un asistente multifase.

Problemas con el renderizado local

Renderizar un IdentificationModal en cada fila de la tabla crea miles de elementos DOM ocultos. El componente está repleto de tablas, inputs enmascarados, hooks de RTK Query y un stepper: el uso de memoria se dispara.

Google AdInline article slot

Aquí va un ejemplo malo:

const ProductRow = ({ product }) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <tr>
      <td>{product.name}</td>
      <td>
        <Button onClick={() => setIsOpen(true)}>Agregar identificadores</Button>
        <IdentificationModal
          isOpen={isOpen}
          onClose={() => setIsOpen(false)}
          productId={product.id}
        />
      </td>
    </tr>
  );
};

Lección clave: Los modales deben renderizarse una sola vez a nivel de página.

Perforación de props y estado global

Mover el modal hacia arriba en el árbol soluciona el hinchazón del DOM, pero implica pasar callbacks a través de 4-5 capas de componentes:

Google AdInline article slot
  • ProductsPageProductsTableTableBodyProductRowActionsMenu

Usar Redux con dispatch de openIdent(productId) elimina los props, pero se complica con múltiples contextos.

Múltiples fuentes de datos

El modal obtiene datos de tres fuentes con APIs distintas:

| Fuente de activación | Pedidos | Carga de datos | Plantilla Excel |

Google AdInline article slot

|---------------|--------|--------------|----------------|

| Encabezado de tabla | Todos los pedidos | ordersApi.getIdentOrders() | ordersApi.getExcel() |

| Fila de tabla | Pedido único | Filtrar por orderId | ordersApi.getExcel() |

| Carrito | Pedidos del carrito | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel() |

Amontonar la lógica en el modal lo convierte en un Objeto Dios lleno de sentencias if, invalidación de caché y analíticas. La alternativa —un modal UI puro con props orders y onSave— falla porque la página no conoce el contexto de antemano.

Dependencias de modales

Abrir el modal de identificadores oculta el panel de acciones masivas; cerrarlo lo restaura. Agregar un modal de confirmación ("¿Cambios sin guardar?") implica manejar manualmente banderas:

openConfirmClose: (state) => {
  state.isConfirmOpen = true;
  state.isIdentOpen = false;
},
cancelConfirmClose: (state) => {
  state.isConfirmOpen = false;
  state.isIdentOpen = true;
},

Estructuras de banderas planas no capturan el orden de apilamiento ni estrategias de rollback.

Arquitectura de solución: Pila tipada

Este sistema resuelve tres problemas de una vez:

  • Renderizado único mediante una pila Redux con cola de superposiciones.
  • Separación de contextos a través de wrappers de fuente.
  • Gestión automática de dependencias con reglas de pila.

Slice de Redux para la pila

export enum EOverlayName {
  BulkActions = 'bulkActions',
  IdentFromHeader = 'identFromHeader',
  IdentFromRow = 'identFromRow',
  IdentFromCart = 'identFromCart',
  EditProduct = 'editProduct',
  // ...
}

type OverlayItem<TData = unknown> = {
  name: EOverlayName;
  data: TData;
  priority?: number;
};

type OverlayStack = OverlayItem[];

La pila almacena datos tipados y orden. Acciones asíncronas empujan superposiciones con payloads.

Hooks de superposiciones para activadores

const useOverlay = () => {
  const dispatch = useDispatch();

  const open = <TData>(name: EOverlayName, data: TData) => {
    dispatch(openOverlay({ name, data }));
  };

  const close = (name: EOverlayName) => {
    dispatch(closeOverlay(name));
  };

  return { open, close };
};

Botón de fila: open('identFromRow', { productId, orderId }). El modal lee de la pila —sin perforación de props.

Wrappers de fuente para contextos

Cada fuente exporta un adaptador:

// identFromRow.adapter.ts
export const useIdentFromRowAdapter = () => {
  const { open } = useOverlay();

  const openIdent = (productId: string, orderId: string) => {
    const orders = ordersApi.useGetIdentOrdersQuery();
    const filtered = orders.data?.filter(o => o.id === orderId);
    open('identFromRow', { orders: filtered, productId });
  };

  return { openIdent };
};

El modal recibe orders y onSave preparados sin conocer la fuente.

Estrategias de apertura/cierre

La pila impone reglas:

  • Reemplazo: Abrir ident cierra bulkActions.
  • Pausa: Cerrar reanuda la superposición anterior.
  • Limpieza: Confirmación resetea la pila.
const openOverlay = createAsyncThunk(
  'overlay/open',
  async ({ name, data }, { getState }) => {
    const stack = selectStack(getState());
    // Lógica de reemplazo/prioridad
    return { name, data };
  }
);

Beneficios clave

  • La pila elimina perforación de props, duplicación de DOM y dependencias de estado con una sola arquitectura.
  • Los wrappers de fuente aíslan la lógica de API; los modales permanecen como UI pura.
  • Tipado OverlayItem<TData> garantiza seguridad de datos.
  • Estrategias asíncronas automatizan el orden de apertura/cierre.
  • Escala a docenas de modales sin inflar el reducer.

— Editorial Team

Advertisement 728x90

Leer después