React封装过哪些组件-下拉选择器和弹窗表单
背景(S - Situation):在某活动管理系统中,前端页面需要支持用户选择“要配置的当前活动”,并提供「新增」「编辑」功能,操作内容包括填写活动名称、ID、版本号等字段。原始实现逻辑分散、复用性差,不利于维护和功能拓展。
目标(T - Task):
封装一个通用的 ActivitySelector
组件,支持以下功能:
✅ 异步加载活动列表,支持 loading 状态;
✅ 支持新增 / 编辑活动信息,并自动更新下拉框内容;
✅ 支持禁用某些选项、设置默认值;
✅ 弹窗使用 antd.Modal
⚠️ 实现重难点总结
1. 组件对外暴露弹窗控制器(Context 模式)
❗ 难点在于:如何在父组件中控制子组件内部的 Modal 行为
采用 React 的
createContext
+useContext
创建全局控制器;内部封装了
openModal()
方法,供外部调用;父组件通过
useActivityModal()
获取控制器,实现跨组件通信;解耦了弹窗的触发逻辑,使组件更灵活、可扩展。
👉 适用于复杂业务流程、URL 参数触发弹窗、全局快捷操作等场景。
2. 异步数据加载与状态同步
初始加载通过
fetchOptions
动态获取活动列表;新增或编辑成功后自动刷新并回填选项;
异步处理流程中加入
loading
状态,保证用户体验;使用
Form.setFieldsValue()
动态填充表单数据。
3. 新增/编辑共用同一个 Modal + 表单
通过
editItem
区分是“新增”还是“编辑”状态;弹窗标题、确认逻辑复用,简化了 UI;
校验规则、默认值、字段配置均可灵活拓展。
4. 选项禁用处理 + 名称唯一性校验
支持传入
disabledIds
数组动态禁用某些选项;在表单提交时手动校验名称重复,防止业务错误;
校验逻辑抽离出来便于维护或拓展为服务端验证。
5. 默认版本号处理
若用户未填写版本号,自动填充为指定
defaultVersion
;避免每次用户手动输入,提高使用体验。
当然可以,咱来详细解释一下这句:
✅ 「对外暴露 context 控制器,支持父组件主动打开弹窗」是什么意思?
🔧 背景场景
我们在封装一个组件(比如 <ActivitySelector />
)时,通常 弹窗的打开/关闭是组件内部控制的,比如用户点击“新增”或“编辑”按钮时,组件内部去 setModalVisible(true)
打开弹窗。
但有时候你会希望 在组件外部 主动打开弹窗,例如:
有一个按钮在父组件中,点击它时希望直接打开弹窗(比如预设一个新活动);
希望根据某个外部逻辑(例如 URL 参数)控制弹窗的显示;
在表单联动或流程中,用户完成前一步操作后触发弹窗。
📦 如何实现?
这就需要组件对外暴露一个控制器(Controller),最常见的方式就是使用 React 的 Context + useContext
+ Provider
。
✅ 具体做法举例(摘自你项目中的代码)
// 创建一个 context(上下文对象)
const ActivityModalContext = createContext(null);// 暴露一个 Hook,让外部能使用这个控制器
export const useActivityModal = () => useContext(ActivityModalContext);
然后在组件内部:
<ActivityModalContext.Provider value={{ openModal }}>{/* 组件内容 */}
</ActivityModalContext.Provider>
这样父组件就可以写成这样:
import { useActivityModal } from './ActivitySelector';function ParentComponent() {const { openModal } = useActivityModal();return <Button onClick={() => openModal({ id: 'xxx' })}>外部打开弹窗</Button>;
}
✅ 总结:这是什么意思?
「对外暴露 context 控制器」就是指:
封装组件时,借助 React 的 Context 机制,把内部的控制方法(如打开弹窗)暴露出去,让外部组件也能调用它。
🚀 好处
好处 | 说明 |
---|---|
📦 解耦 | 父组件无需知道 Modal 是怎么实现的,只要能打开它就行 |
🧩 灵活 | 可以在任何地方调用 openModal ,比如 URL 路由、定时器、其他组件事件等 |
👨💻 易于测试与复用 | 可以单独测试控制器逻辑,也可以跨页面/组件共享 |
✅ ActivitySelector.tsx
完整封装代码
import React, { useState, useEffect, createContext, useContext } from 'react';
import { Select, Modal, Form, Input, Button, message } from 'antd';const { Option } = Select;// --------- Context 控制器 ---------
const ActivityModalContext = createContext(null);export const useActivityModal = () => useContext(ActivityModalContext);// --------- 主组件封装 ---------
const ActivitySelector = ({value,onChange,fetchOptions,onCreate,onEdit,disabledIds = [],defaultVersion = 'v1.0',
}) => {const [options, setOptions] = useState([]);const [loading, setLoading] = useState(false);const [modalVisible, setModalVisible] = useState(false);const [editItem, setEditItem] = useState(null);const [form] = Form.useForm();// 初始化加载useEffect(() => {loadOptions();}, []);const loadOptions = async () => {setLoading(true);const data = await fetchOptions?.();setOptions(data || []);setLoading(false);};const openModal = (item = null) => {setEditItem(item);form.setFieldsValue(item || { version: defaultVersion });setModalVisible(true);};const handleOk = async () => {try {const formData = await form.validateFields();// 校验重复名称(或其他字段)const isRepeat = options.some((item) => item.name === formData.name && item.id !== editItem?.id);if (isRepeat) {message.error('活动名称重复,请重新输入');return;}const result = editItem? await onEdit?.(editItem.id, formData): await onCreate?.(formData);await loadOptions();onChange?.(result); // 自动选中新项setModalVisible(false);} catch (err) {console.error('表单提交失败', err);}};return (<ActivityModalContext.Provider value={{ openModal }}><div style={{ display: 'flex', gap: 8 }}><Selectvalue={value?.id}onChange={(val) => {const selected = options.find((item) => item.id === val);onChange?.(selected);}}loading={loading}style={{ flex: 1 }}placeholder="请选择活动"allowClearshowSearchoptionFilterProp="children">{options.map((item) => (<Option key={item.id} value={item.id} disabled={disabledIds.includes(item.id)}>{item.name}({item.version})</Option>))}</Select><Button onClick={() => openModal()}>新增</Button><Button onClick={() => openModal(value)} disabled={!value}>编辑</Button></div><Modaltitle={editItem ? '编辑活动' : '新增活动'}open={modalVisible}onCancel={() => setModalVisible(false)}onOk={handleOk}destroyOnClose><Form form={form} layout="vertical" preserve={false}><Form.Itemlabel="活动名称"name="name"rules={[{ required: true, message: '请输入活动名称' }]}><Input /></Form.Item><Form.Itemlabel="活动 ID"name="id"rules={[{ required: true, message: '请输入活动 ID' }]}><Input /></Form.Item><Form.Itemlabel="版本号"name="version"rules={[{ required: true, message: '请输入版本号' }]}><Input placeholder="如 v1.0" /></Form.Item><Form.Item label="扩展信息" name="meta"><Input.TextArea placeholder="可以是 JSON、备注等" /></Form.Item></Form></Modal></ActivityModalContext.Provider>);
};export default ActivitySelector;
🧪 外部调用方式(使用 Context 控制器)
import React, { useState, useEffect } from 'react';
import ActivitySelector, { useActivityModal } from './ActivitySelector';const ParentPage = () => {const [selected, setSelected] = useState(null);const { openModal } = useActivityModal();const fetchOptions = async () => {return [{ id: 'act001', name: '暑期促销', version: 'v1.0', meta: '' },{ id: 'act002', name: '双11预热', version: 'v1.2', meta: '' },];};const handleCreate = async (data) => {// 模拟添加后返回新数据项return { id: 'act003', ...data };};const handleEdit = async (id, data) => {return { id, ...data };};return (<div><h2>活动管理</h2><ActivitySelectorvalue={selected}onChange={setSelected}fetchOptions={fetchOptions}onCreate={handleCreate}onEdit={handleEdit}disabledIds={['act002']}defaultVersion="v2.0"/><Button onClick={() => openModal()}>🔧 外部控制打开弹窗</Button></div>);
};export default ParentPage;
如你有后续需求(如:分页、远程搜索、多选、自定义弹窗样式或 MUI 替换),可以继续在这个封装基础上扩展,我也可以帮你一步步完成。需要我继续用 STAR 法则
补充笔记内容吗?