React高质量组件开发——复合组件模式的应用
背景介绍
这是学习设计模式的第十四章,学习的是复合模式内容。记录的是自己学习理解的过程,欢迎大家讨论。
关于设计模式前十三节的内容,可以关注React设计模式专栏获取更多内容。
极简释义
复合组件模式:使用多个组件协同完成单一功能。
正文
在我们的项目中,经常有很多组件。有些组件通过共享state,或者共享逻辑,相互依赖。复合组件常见于select
,menu
,dropdown
组件和他们的子项之间。复合组件模式正是通过创建多个互相协作组件共同完成一个功能。
Context API
下面我们通过一个图片列表组件来具体阐述一下复合组件模式。比如说有一个图片列表组件,展示很多松鼠🐿图片,当然不仅展示松鼠图片,我们希望用户可以编辑和删除图片,所以我们给每个图片添加一个操作按钮。这时我们可以创建一个FlyOut
组件,用来展示用户点击操作按钮后下拉弹出的操作菜单,如下图所示:

分析:要实现一个下拉菜单组件,我们需要拆分成三个组件:
- 一个容器组件
FlyOut
,state状态,容纳操作按钮和下拉菜单; - 一个操作按钮
Toggle
,让用户点击; - 一个列表菜单
List
,让用户选择操作;
使用React Context API来实现这样一个下拉菜单组件是一个很好的共享状态的方案。
首先,我们实现容器组件FlyOut
,这个组件包含state,返回一个Context.Provider,为子组件提供所需要的open状态。
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, setOpen] = useState(false);
return (
<FlyOutContext.Provider value={{ open, setOpen }}>
{props.children}
</FlyOutContext.Provider>
);
}
现在我们有了一个状态组件FlyOut
,并且为所有的子组件提供了open状态和修改open状态的方法。
接下来我们来实现操作按钮组件Toggle
,这个组件仅展示操作按钮,并修改FlyOut
的open状态。
function Toggle() {
const { open, setOpen } = useContext(FlyOutContext);
return (
<div onClick={() => setOpen(!open)}>
<Icon />
</div>
);
}
为了让Toggle
组件能正确地使用FlyOutContext,我们需要把Toggle
组件用做FlyOut
的children,当然我们也可以把Toggle
组件作为FlyOut
组件的一个属性,正如antd组件库Slect.Option
那样的形式。
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
);
}
function Toggle() {
const { open, toggle } = useContext(FlyOutContext);
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
);
}
// 用作FlyOut的一个属性
FlyOut.Toggle = Toggle;
这样做的一个原因一方面是Toggle
组件不能脱离FlyOut
单独使用,另一方面,在引入组件时,只需要引入FlyOut
就可以。
import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
</FlyOut>
);
}
接下来我们来开发List
组件,List
组件依赖FlyOutContext的open属性:
function List({ children }) {
const { open } = React.useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function Item({ children }) {
return <li>{children}</li>;
}
List
组件的children可能有多个Item
,我们可以像Toggle
组件一样,把List
和Item
组件作为FlyOut
的一个属性,简化组件引入;
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
);
}
function Toggle() {
const { open, toggle } = useContext(FlyOutContext);
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
);
}
function List({ children }) {
const { open } = useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function Item({ children }) {
return <li>{children}</li>;
}
// 把Toggle、List、Item作为FlyOut的属性
FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;
接下来我们看怎么使用FlyOut
这个组件:
import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</FlyOut>
);
}
现在我们FlyOutMenu
组件不再需要任何state,只需要引入FlyOut
组件就可以了。
当我们开发多个相互依赖的组件时,复合模式就很有用,相信大家在使用antd等优秀组件库时,就会发现这种模式比较常见。
React.Children.map
当然除了使用Context外,我们也可以使用React.Children.map结合React.cloneElement把open和setOpen方法传递给子组件:
export function FlyOut(props) {
const [open, setOpen] = React.useState(false);
return (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { open, setOpen })
)}
</div>
);
}
想想大家能看懂,Children.map方法会遍历所有的children,通过第二个参数,把open和setOpen映射给循环子组件;需要注意的是,这里的map方法要和数组的map方法对比记忆一下。
下面我们来看具体的实现代码:
import React from "react";
import Icon from "./Icon";
export function FlyOut(props) {
const [open, toggle] = React.useState(false);
return (
<div className={`flyout`}>
{React.Children.map(props.children, child =>
React.cloneElement(child, { open, toggle })
)}
</div>
);
}
function Toggle({ open, toggle }) {
return (
<div className="flyout-btn" onClick={() => toggle(!open)}>
<Icon />
</div>
);
}
function List({ children, open }) {
return open && <ul className="flyout-list">{children}</ul>;
}
function Item({ children }) {
return <li className="flyout-item">{children}</li>;
}
FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;
总结
复合模式维护内部state,并共享给多个不同的子组件;当为复合模式添加新组件时,就不用再考虑自己维护state。
当引入复合模式的组件时,我们不用再单独引入模式内的子组件。
相较于FlyOutContext.Provider,React.Children.map这种传递state的方式只能向直接子组件提供,不能为嵌套更深的子组件传递state,同时意味着父级和子级组件中不能有其他元素比如div:
export default function FlyoutMenu() {
return (
<FlyOut>
{/*ERROR This breaks */}
<div>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</div>
</FlyOut>
);
}
另外React.cloneElement只能进行浅合并,所以存在props命名冲突、覆盖问题。
相关活动
转载自:https://juejin.cn/post/7211744771949183036