设计模式
复合模式
在我们的应用程序中,我们经常有相互关联的组件。它们通过共享状态相互依赖,并共同分享逻辑。你经常在 select
、下拉组件或菜单项等组件中看到这种情况。复合组件模式允许你创建协同工作以完成任务的组件。
Context API
让我们看一个例子:我们有一系列松鼠图片!除了展示松鼠图片外,我们还想要添加一个按钮,使用户可以编辑或删除图片。我们可以实现一个 FlyOut
组件,当用户切换组件时显示一个列表。
在 FlyOut
组件中,我们本质上有三件事
FlyOut
容器,它包含切换按钮和列表Toggle
按钮,用于切换List
List
,包含菜单项列表
使用 React 的 Context API 与复合组件模式完美契合这个例子!
首先,让我们创建 FlyOut
组件。该组件保存状态,并返回一个带有切换值的 FlyOutProvider
给它接收的所有子组件。
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
);
}
现在我们有了具有状态的 FlyOut
组件,它可以将 open
和 toggle
的值传递给它的子组件!
让我们创建 Toggle
组件。该组件只是渲染用户可以点击以切换菜单的组件。
function Toggle() {
const { open, toggle } = useContext(FlyOutContext);
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
);
}
为了真正让 Toggle
访问 FlyOutContext
提供者,我们需要将其作为 FlyOut
的子组件渲染!我们可以简单地将其作为子组件渲染。但是,我们也可以将 Toggle
组件设为 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>
);
}
FlyOut.Toggle = Toggle;
这意味着如果我们想要在任何文件中使用 FlyOut
组件,我们只需导入 FlyOut
即可!
import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
</FlyOut>
);
}
仅仅一个切换是不够的。我们还需要一个 List
,包含列表项,这些列表项根据 open
的值打开和关闭。
function List({ children }) {
const { open } = React.useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function Item({ children }) {
return <li>{children}</li>;
}
List
组件根据 open
的值是 true
还是 false
来渲染其子组件。让我们将 List
和 Item
设为 FlyOut
组件的属性,就像我们对 Toggle
组件所做的那样。
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>;
}
FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;
现在我们可以将它们作为 FlyOut
组件的属性使用!在这种情况下,我们希望向用户显示两个选项:编辑和删除。让我们创建一个 FlyOut.List
,它渲染两个 FlyOut.Item
组件,一个用于编辑选项,另一个用于删除选项。
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>
);
}
完美!我们刚刚创建了一个完整的 FlyOut
组件,而无需在 FlyOutMenu
本身添加任何状态!
1import React from "react";2import "./styles.css";3import { FlyOut } from "./FlyOut";45export default function FlyoutMenu() {6 return (7 <FlyOut>8 <FlyOut.Toggle />9 <FlyOut.List>10 <FlyOut.Item>Edit</FlyOut.Item>11 <FlyOut.Item>Delete</FlyOut.Item>12 </FlyOut.List>13 </FlyOut>14 );15}
当你在构建组件库时,复合模式非常有用。当你使用像 Semantic UI 这样的 UI 库时,你经常会看到这种模式。
React.Children.map
我们也可以通过遍历组件的子组件来实现复合组件模式。我们可以通过 克隆 它们并添加额外的 props 来为这些元素添加 open
和 toggle
属性。
export function FlyOut(props) {
const [open, toggle] = React.useState(false);
return (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { open, toggle })
)}
</div>
);
}
所有子组件都被克隆,并传递了 open
和 toggle
的值。现在,我们无需像上一个例子那样使用 Context API,就可以通过 props
访问这两个值。
1import React from "react";2import Icon from "./Icon";34const FlyOutContext = React.createContext();56export function FlyOut(props) {7 const [open, toggle] = React.useState(false);89 return (10 <div>11 {React.Children.map(props.children, child =>12 React.cloneElement(child, { open, toggle })13 )}14 </div>15 );16}1718function Toggle() {19 const { open, toggle } = React.useContext(FlyOutContext);2021 return (22 <div className="flyout-btn" onClick={() => toggle(!open)}>23 <Icon />24 </div>25 );26}2728function List({ children }) {29 const { open } = React.useContext(FlyOutContext);30 return open && <ul className="flyout-list">{children}</ul>;31}3233function Item({ children }) {34 return <li className="flyout-item">{children}</li>;35}3637FlyOut.Toggle = Toggle;38FlyOut.List = List;39FlyOut.Item = Item;
优点
复合组件管理它们自己的内部状态,这些状态在多个子组件之间共享。在实现复合组件时,我们无需担心自己管理状态。
在导入复合组件时,我们无需显式导入该组件上可用的子组件。
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>
);
}
缺点
当使用 React.Children.map
来提供值时,组件嵌套受到限制。只有父组件的直接子组件才能访问 open
和 toggle
props,这意味着我们不能将任何这些组件包装在另一个组件中。
export default function FlyoutMenu() {
return (
<FlyOut>
{/* This breaks */}
<div>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</div>
</FlyOut>
);
}
使用 React.cloneElement
克隆元素会执行浅合并。已经存在的 props 将与我们传递的新 props 合并在一起。如果已经存在的 prop 与我们传递给 React.cloneElement
方法的 props 有相同的名称,则可能会导致命名冲突。由于 props 是浅合并的,因此该 prop 的值将被我们传递的最新值覆盖。