返回首页

React 模态堆栈:IT 架构

本文描述了 React 应用的类型化模态堆栈架构。通过 Redux slice、overlay hooks 和源包装器解决 DOM 重复问题、prop drilling 和依赖问题。提供代码示例和扩展策略。

React 中用于复杂 UI 的类型化模态堆栈
Advertisement 728x90

React 模态栈架构:打造可扩展应用

在复杂的 React 应用中,模态框很快就会成为开发痛点。单个模态框用基本的 useState 还能凑合,但项目一壮大,就面临组件重复、属性穿透和状态依赖纠缠的难题。解决方案?一个类型安全的覆盖栈,按触发源分离逻辑。

想象一个典型的电商管理后台,产品表格中,每行都需要一个模态框来添加标识符(IMEI、SGTIN),里面包含订单表格、验证、Excel 上传和多步骤向导。

本地渲染的痛点

在表格每行都渲染 IdentificationModal,会产生数千个隐藏 DOM 元素。组件内塞满了表格、掩码输入、RTK Query hooks 和步骤器——内存占用直线上升。

Google AdInline article slot

来看个反面教材:

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

  return (
    <tr>
      <td>{product.name}</td>
      <td>
        <Button onClick={() => setIsOpen(true)}>添加标识符</Button>
        <IdentificationModal
          isOpen={isOpen}
          onClose={() => setIsOpen(false)}
          productId={product.id}
        />
      </td>
    </tr>
  );
};

核心要点: 模态框应在页面顶层只渲染一次。

属性穿透与全局状态

把模态框提到上层,能解决 DOM 膨胀,但得通过 4-5 层组件传递回调:

Google AdInline article slot
  • ProductsPageProductsTableTableBodyProductRowActionsMenu

用 Redux 的 openIdent(productId) 派发能省掉属性,但多上下文时就乱套了。

多数据源挑战

模态框从三个不同 API 拉取数据:

| 触发源 | 订单数据 | 数据加载 | Excel 模板 |

Google AdInline article slot

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

| 表格头部 | 全部订单 | ordersApi.getIdentOrders() | ordersApi.getExcel()|

| 表格行 | 单个订单 | 按 orderId 过滤 | ordersApi.getExcel()|

| 购物车 | 购物车订单 | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel()|

把逻辑全塞进模态框,会变成上帝组件,满是 if 判断、缓存失效和埋点。另一种纯 UI 模态(带 ordersonSave 属性)又不行,因为页面事先不知上下文。

模态框依赖关系

打开标识符模态时,批量操作面板隐藏;关闭后恢复。加个确认模态("有未保存变更?")就得手动切换标志:

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

平面标志结构无法捕捉层叠顺序或回滚策略。

解决方案:类型安全栈

这个系统一次性解决三大痛点:

  • 单次渲染:通过 Redux 栈 + 覆盖队列。
  • 上下文分离:用源适配器封装。
  • 自动依赖管理:栈规则自动处理。

栈的 Redux Slice

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

栈保存类型化数据和顺序。异步 action 推送覆盖层及载荷。

触发钩子

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

行按钮:open('identFromRow', { productId, orderId })。模态直接读栈——零属性穿透。

源适配器封装上下文

每个源导出适配器:

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

模态拿到预处理 ordersonSave,无需知晓源头。

开关策略

栈强制执行规则:

  • 替换:打开 ident 关闭 bulkActions
  • 暂停:关闭时恢复前一层。
  • 清栈:确认后重置栈。
const openOverlay = createAsyncThunk(
  'overlay/open',
  async ({ name, data }, { getState }) => {
    const stack = selectStack(getState());
    // 替换/优先级逻辑
    return { name, data };
  }
);

核心优势

  • 栈架构一举消除属性穿透、DOM 重复和状态依赖。
  • 源适配器隔离 API 逻辑,模态保持纯 UI。
  • OverlayItem<TData> 类型确保数据安全。
  • 异步策略自动管理开关顺序。
  • 轻松扩展数十个模态,不臃肿 reducer。

— Editorial Team

Advertisement 728x90

继续阅读