React 模态栈架构:打造可扩展应用
在复杂的 React 应用中,模态框很快就会成为开发痛点。单个模态框用基本的 useState 还能凑合,但项目一壮大,就面临组件重复、属性穿透和状态依赖纠缠的难题。解决方案?一个类型安全的覆盖栈,按触发源分离逻辑。
想象一个典型的电商管理后台,产品表格中,每行都需要一个模态框来添加标识符(IMEI、SGTIN),里面包含订单表格、验证、Excel 上传和多步骤向导。
本地渲染的痛点
在表格每行都渲染 IdentificationModal,会产生数千个隐藏 DOM 元素。组件内塞满了表格、掩码输入、RTK Query hooks 和步骤器——内存占用直线上升。
来看个反面教材:
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 层组件传递回调:
ProductsPage→ProductsTable→TableBody→ProductRow→ActionsMenu
用 Redux 的 openIdent(productId) 派发能省掉属性,但多上下文时就乱套了。
多数据源挑战
模态框从三个不同 API 拉取数据:
| 触发源 | 订单数据 | 数据加载 | Excel 模板 |
|------------|--------------|------------------------------|-----------------------|
| 表格头部 | 全部订单 | ordersApi.getIdentOrders() | ordersApi.getExcel()|
| 表格行 | 单个订单 | 按 orderId 过滤 | ordersApi.getExcel()|
| 购物车 | 购物车订单 | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel()|
把逻辑全塞进模态框,会变成上帝组件,满是 if 判断、缓存失效和埋点。另一种纯 UI 模态(带 orders 和 onSave 属性)又不行,因为页面事先不知上下文。
模态框依赖关系
打开标识符模态时,批量操作面板隐藏;关闭后恢复。加个确认模态("有未保存变更?")就得手动切换标志:
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 };
};
模态拿到预处理 orders 和 onSave,无需知晓源头。
开关策略
栈强制执行规则:
- 替换:打开
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
暂无评论。