在应用程序中,经常有属于彼此的组件。
它们通过共享状态相互依赖,并共享逻辑。
经常会在选择、下拉组件或菜单项等组件中看到这一点。
复合组件模式允许创建所有一起工作以执行任务的组件。
Context API
让我们看一个例子:
我们有一个松鼠图像列表!
除了只显示松鼠图像之外,我们还想添加一个按钮,使用户可以编辑或删除图像。
我们可以实现一个 FlyOut 组件,当用户切换组件时显示一个列表。
在 FlyOut 组件中,我们基本上拥有三样东西:
- FlyOut 包装器,其中包含切换按钮和列表
- Toggle 按钮,用于切换列表
- List ,其中包含菜单项列表
使用复合组件模式和 React 的 Context API 非常适合这个例子!
首先,让我们创建 FlyOut 组件。 该组件保持状态,并将带有切换值的 FlyOutProvider 返回给它接收到的所有子项。
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
const providerValue = { open, toggle };
return (
<FlyOutContext.Provider value={providerValue}>
{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>
);
}
Perfect! 我们刚刚创建了一个完整的 FlyOut 组件,而没有在 FlyOutMenu 本身中添加任何状态!
当您构建组件库时,复合模式非常有用。 在使用语义 UI 等 UI 库时,您会经常看到这种模式。
React.Children.map
我们还可以通过映射组件的子组件来实现复合组件模式。
我们可以将 open 和 toggle 属性添加到这些元素中,通过使用额外的 props 克隆它们。
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 访问这两个值。
优点
复合组件管理它们自己的内部状态,它们在几个子组件之间共享。
在实现复合组件时,我们不必担心自己管理状态。
导入复合组件时,我们不必显式导入该组件上可用的子组件。
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 克隆元素执行浅合并。 已经存在的道具将与我们传递的新道具合并在一起。
如果已经存在的 prop 与我们传递给 React.cloneElement 方法的 props 具有相同的名称,这可能会导致命名冲突。
由于 props 是浅合并的,props 的值将被我们传递的最新值覆盖。
知识点
- createContext()
- useContext()
- Provider
- React.Children.map()