- Published on
React设计模式 —— 复合模式
- Authors
- Name
- Deng Hua
文章翻译至: Compound Pattern
在我们的应用程序中,我们经常拥有彼此所属的组件。它们通过共享状态相互依赖,并共享逻辑。您经常会在选择 、下拉组件或菜单项等组件中看到这种情况。复合组件模式允许你创建所有组件一起运行来执行。
目录
Context API
让我们看一个例子:我们有一个松鼠图像列表!除了仅显示松鼠图像之外,我们还想添加一个按钮,使用户可以编辑或删除图像。我们可以实现一个 FlyOut
组件,当用户切换该组件时,它会显示一个列表。
在 FlyOut
组件中,我们基本上有三件事:
FlyOut wrapper,其中包含切换按钮和列表
Toggle 按钮,用于切换 List
List ,包含菜单项列表
首先,让我们创建 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>
);
}
为了让FlyOutContext
Provider可以访问Toggle
,我们需要将其渲染为 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>
);
}
仅一个toggle是不够的。我们还需要一个带有列表项的 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
本身中添加任何状态!
当您构建组件库时,复合模式非常有用。使用 Semantic UI 等 UI 库时,您经常会看到这种模式。
React.Children.map
我们还可以通过映射组件的子组件来实现复合组件模式。我们可以通过使用附加属性克隆它们来向这些元素添加 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
的值。我们现在可以通过 props
访问这两个值,而不必像前面的示例那样使用 Context API。
优点
复合组件管理自己的内部状态,并在多个子组件之间共享这些状态。在实现复合组件时,我们不必担心自己管理状态。
导入复合组件时,我们不必显式导入该组件上可用的子组件。
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
属性,这意味着我们无法将这些组件中的任何一个包装在另一个组件中。
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
合并在一起。
如果已经存在的 props
与我们传递给 React.cloneElement
方法的 props
同名,这可能会导致命名冲突。当 props 被浅层合并时,该 prop
的值将被我们传递的最新值覆盖。
End.