Back to Home

React Modal Stack: Architecture for IT

The article describes the architecture of a typed modal stack for React applications. Solves DOM duplication problems, prop drilling, and dependencies via Redux slice, overlay hooks, and source wrappers. Provides code examples and scaling strategies.

Typed Modals Stack in React for Complex UI
Advertisement 728x90

React Modal Stack Architecture for Scalable Apps

In complex React apps, modals quickly become a headache. A single modal with basic useState works fine in isolation, but as the project grows, you end up with duplicated components, prop drilling, and tangled state dependencies. The fix? A typed overlay stack that separates logic by trigger source.

Take a typical marketplace admin dashboard with a products table. Each row needs a modal for adding identifiers (IMEI, SGTIN), complete with order tables, validation, Excel uploads, and a multi-step wizard.

Problems with Local Rendering

Rendering an IdentificationModal in every table row creates thousands of hidden DOM elements. The component is packed with tables, masked inputs, RTK Query hooks, and a stepper—memory usage skyrockets.

Google AdInline article slot

Here's a bad example:

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

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

Key takeaway: Modals should render once at the page level.

Prop Drilling and Global State

Moving the modal up the tree fixes the DOM bloat but means passing callbacks through 4–5 component layers:

Google AdInline article slot
  • ProductsPageProductsTableTableBodyProductRowActionsMenu

Redux with openIdent(productId) dispatches eliminates props, but gets messy with multiple contexts.

Multiple Data Sources

The modal pulls from three spots with different APIs:

| Trigger Source | Orders | Data Loading | Excel Template |

Google AdInline article slot

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

| Table Header | All orders | ordersApi.getIdentOrders() | ordersApi.getExcel() |

| Table Row | Single order | Filter by orderId | ordersApi.getExcel() |

| Cart | Cart orders | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel() |

Cramming logic into the modal turns it into a God Object full of if statements, cache invalidation, and analytics. The alternative—a pure UI modal with orders and onSave props—fails because the page doesn't know the context upfront.

Modal Dependencies

Opening the identifier modal hides the bulk actions panel; closing it brings it back. Adding a confirmation modal ("Unsaved changes?") means manually juggling flags:

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

Flat flag structures don't capture stacking order or rollback strategies.

Solution Architecture: Typed Stack

This system tackles three issues at once:

  • Single rendering via a Redux stack with an overlay queue.
  • Context separation through source wrappers.
  • Automatic dependency management with stack rules.

Redux Slice for the Stack

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[];

The stack holds typed data and order. Async actions push overlays with payloads.

Overlay Hooks for Triggers

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 };
};

Row button: open('identFromRow', { productId, orderId }). The modal reads from the stack—no prop drilling.

Source Wrappers for Contexts

Each source exports an adapter:

// 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 };
};

The modal gets prepped orders and onSave without knowing the source.

Open/Close Strategies

The stack enforces rules:

  • Replacement: Opening ident closes bulkActions.
  • Pause: Closing resumes the previous overlay.
  • Clear: Confirmation resets the stack.
const openOverlay = createAsyncThunk(
  'overlay/open',
  async ({ name, data }, { getState }) => {
    const stack = selectStack(getState());
    // Replacement/priority logic
    return { name, data };
  }
);

Key Benefits

  • The stack eliminates prop drilling, DOM duplication, and state dependencies with one architecture.
  • Source wrappers isolate API logic; modals stay pure UI.
  • OverlayItem<TData> typing ensures data safety.
  • Async strategies automate open/close order.
  • Scales to dozens of modals without bloating the reducer.

— Editorial Team

Advertisement 728x90

Read Next