在React的开发中,我们经常会遇到这样的场景:组件之间需要共享状态,并且根据用户的交互或业务需求,多个组件需要协同工作。这时,设计出既灵活又可维护的组件结构就变得非常重要。复合组件模式 (Compound Component Pattern) 就是一种很好的设计模式,它允许父组件提供共享的状态,子组件则负责完成具体的任务。
概念
复合组件模式指的是,将一个功能拆分成多个子组件,通过上下文(Context)进行状态共享,父组件负责提供核心逻辑,子组件则根据需要访问状态或操作函数。它让父子组件的通信更加直观,也能大大提升组件的灵活性与可扩展性。
使用场景
1. 可重用的交互组件
当我们需要构建一个交互组件,如模态窗口,标签页或者表单时,这些组件往往由多个部分组成,例如模态窗口: 展示窗口,背景模态,打开和关闭窗口的组件等。在这种情况下,复合组件模式非常适用,它让你能够以一种声明式的方式构建组件,同时保持各个部分的分离。
2. 需要灵活组合的组件
有些场景下,用户可能需要以不同的方式组合和使用组件。例如,在表单中,输入框和按钮是常见的组合,但不同页面可能对这些组件的布局或功能有不同的要求。复合组件模式允许开发者灵活地组合这些子组件,轻松适应各种需求。
为什么使用复合组件模式
1. 提升组件的灵活性
复合组件模式允许开发者根据具体的使用场景来组合子组件。每个子组件专注于完成自己的任务,而父组件负责管理状态。这种解耦设计让组件更加灵活,父组件可以随意调整子组件的数量或结构。
2. 避免 prop drilling(属性层层传递)
传统的组件通信往往需要通过一层层地传递属性,这种方式不仅繁琐,还会导致组件之间的耦合度过高。复合组件模式通过使用 Context
来共享状态,避免了这种属性传递的麻烦,子组件可以直接访问需要的状态或方法。
3. 增强可维护性
组件拆分清晰,逻辑职责分明,使得代码更加易于维护。如果某个功能需要调整,开发者只需修改对应的子组件,而不必担心对整个组件造成影响。
实践展示
我以一个模态窗口为例来展示。正常来讲一般会是这么构建一个模态窗口组件:
1
2
3
4
5function Modal({ children, onClose }) {
6 return createPortal(
7 <Overlay>
8 <StyledModal>
9 <Button onClick={onClose}>
10 <HiXMark />
11 </Button>
12
13 <div>{children}</div>
14 </StyledModal>
15 </Overlay>,
16 document.body
17 );
18}
19
20export default Modal;
在外部的使用方法则如下所示:
1function AddUser() {
2 const [isOpenModal, setIsOpenModal] = useState(false);
3
4 return (
5 <div>
6 <Button onClick={() => setIsOpenModal((show) => !show)}>
7 添加成员
8 </Button>
9 {isOpenModal && (
10 <Modal onClose={() => setIsOpenModal(false)}>
11 <CreateUserForm onCloseModal={() => setIsOpenModal(false)} />
12 </Modal>
13 )}
14 </div>
15 );
16}
17
18export default AddUser;
乍一看的话这样写也很不错了,适用了大部分场景,但是当它和别的组件套用时,就会显得很乱,并且假如放在一个选项列表中,连续几个按钮都要打开不同的窗口时,就会显得不那么够用,逻辑不那么清晰了。
下面给出复合组件模式的模态窗口写法:
1
2const ModalContext = createContext();
3
4
5function Modal({ children }) {
6 const [openName, setOpenName] = useState("");
7
8 const close = () => setOpenName("");
9 const open = setOpenName;
10
11 return (
12 <ModalContext.Provider value={{ openName, close, open }}>
13 {children}
14 </ModalContext.Provider>
15 );
16}
17
18
19function Open({ children, opens: opensWindowName }) {
20 const { open } = useContext(ModalContext);
21
22
23 return cloneElement(children, { onClick: () => open(opensWindowName) });
24}
25
26function Window({ children, name }) {
27 const { openName, close } = useContext(ModalContext);
28 const ref = useOutsideClick(close);
29
30
31 if (name !== openName) return null;
32
33 return createPortal(
34 <Overlay>
35 <StyledModal ref={ref}>
36 <Button onClick={close}>
37 <HiXMark />
38 </Button>
39
40 <div>{cloneElement(children, { onCloseModal: close })}</div>
41 </StyledModal>
42 </Overlay>,
43 document.body
44 );
45}
46
47
48Modal.Open = Open;
49Modal.Window = Window;
50
51export default Modal;
那么这样的使用方法就变成了如下方式:
1function AddUser() {
2 return (
3 <div>
4 <Modal>
5 <Modal.Open opens="user-form">
6 <Button>添加成员</Button>
7 </Modal.Open>
8
9 <Modal.Window name="user-form">
10 <CreateUserForm />
11 </Modal.Window>
12
13 {/* 可以添加更多的按钮以打开对应窗口 */}
14 {/* <Modal.Open opens="table">
15 <Button>展示成员</Button>
16
17 </Modal.Open>
18 <Modal.Window name="table">
19 <UserTable />
20 </Modal.Window> */}
21 </Modal>
22 </div>
23 );
24}
25
26export default AddUser;
再例如,我们与一个表格组件 (表格同样为复合模式写法) 去套用模态窗口:
1function CabinRow({ cabin }) {
2 ...
3 return (
4 <Table.Row>
5 ...
6 <div>
7 <Modal>
8 <Menus.Menu>
9 <Menus.Toggle id={cabinId} />
10
11 <Menus.List id={cabinId}>
12 <Menus.Button
13 icon={<HiSquare2Stack />}
14 onClick={handleDuplicate}
15 disabled={isCreating}
16 >
17 Duplicate
18 </Menus.Button>
19
20 <Modal.Open opens="edit">
21 <Menus.Button icon={<HiPencil />}>Edit</Menus.Button>
22 </Modal.Open>
23
24 <Modal.Open opens="delete">
25 <Menus.Button icon={<HiTrash />}>Delete</Menus.Button>
26 </Modal.Open>
27 </Menus.List>
28 </Menus.Menu>
29
30 <Modal.Window name="edit">
31 <CreateCabinForm cabinToEdit={cabin} />
32 </Modal.Window>
33
34 <Modal.Window name="delete">
35 <ConfirmDelete
36 resourceName="cabins"
37 disabled={isDeleting}
38 onConfirm={() => deleteCabin(cabinId)}
39 />
40 </Modal.Window>
41 </Modal>
42 </div>
43 </Table.Row>
44 );
45}
46
47export default CabinRow;
最终可以达成这样的效果,同时逻辑也很清晰明了
表格组件代码如下:
1const TableContext = createContext();
2
3function Table({ columns, children }) {
4 return (
5 <TableContext.Provider value={{ columns }}>
6 <StyledTable role="table">{children}</StyledTable>
7 </TableContext.Provider>
8 );
9}
10
11function Header({ children }) {
12 const { columns } = useContext(TableContext);
13 return (
14 <StyledHeader role="row" columns={columns} as="header">
15 {children}
16 </StyledHeader>
17 );
18}
19
20function Row({ children }) {
21 const { columns } = useContext(TableContext);
22 return (
23 <StyledRow role="row" columns={columns}>
24 {children}
25 </StyledRow>
26 );
27}
28
29function Body({ data, render }) {
30 if (!data.length) return <Empty>暂无数据展示</Empty>;
31
32
33 return <StyledBody>{data.map(render)}</StyledBody>;
34}
35
36Table.Header = Header;
37Table.Body = Body;
38Table.Row = Row;
39Table.Footer = Footer;
40
41export default Table;
复合组件模式的优点总结
- 高灵活性:开发者可以自由组合子组件,扩展或修改子组件也很方便。
- 清晰的职责划分:子组件只关心自己负责的功能,父组件负责提供状态,组件职责明确。
- 简化状态共享:通过
Context
共享状态,避免了繁琐的属性传递,提高了代码的可读性和可维护性。 - 便于扩展:新功能可以通过添加子组件来实现,父组件无需修改,符合开闭原则(OCP)。
结语
复合组件模式为 React 组件的设计提供了一种灵活、优雅的解决方案,特别适用于需要多个组件协同工作的场景。它不仅提升了组件的可复用性,还让代码更加清晰、易于维护。如果你在开发过程中遇到组件拆分和状态共享的需求,不妨试试复合组件模式,相信它能让你的组件设计更加简洁、强大。
希望本文能够帮助你理解并使用复合组件模式,打造更灵活、可维护的 React 组件体系。