compound-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompound Pattern
复合组件模式
In our application, we often have components that belong to each other. They're dependent on each other through the shared state, and share logic together. You often see this with components like , dropdown components, or menu items. The compound component pattern allows you to create components that all work together to perform a task.
select在我们的应用中,经常会有一些彼此关联的组件。它们通过共享状态相互依赖,并且共同分享逻辑。你经常会在、下拉组件或菜单项这类组件中看到这种情况。复合组件模式允许你创建能够协同完成某项任务的组件。
selectWhen to Use
适用场景
- Use this when building components like dropdowns, tabs, or menus with related sub-components
- This is helpful when you want to provide a clean component API without exposing internal state management
- 当你构建下拉菜单、标签页或包含相关子组件的菜单时,可使用该模式
- 如果你希望提供简洁的组件API,同时不暴露内部状态管理逻辑,这种模式会很有帮助
Instructions
实现步骤
- Use React Context API to share state between the parent compound component and its children
- Attach child components as static properties on the parent (e.g., ,
FlyOut.Toggle)FlyOut.List - Memoize context values to avoid unnecessary re-renders in complex scenarios
- Prefer the Context approach over for more flexible component nesting
React.Children.map
- 使用React Context API在父级复合组件及其子组件之间共享状态
- 将子组件作为父组件的静态属性(例如:、
FlyOut.Toggle)FlyOut.List - 在复杂场景下,对上下文值进行记忆化处理以避免不必要的重渲染
- 相比,更推荐使用Context方式,以支持更灵活的组件嵌套
React.Children.map
Details
详细说明
Context API
Context API
Let's look at an example: we have a list of squirrel images! Besides just showing squirrel images, we want to add a button that makes it possible for the user to edit or delete the image. We can implement a component that shows a list when the user toggles the component.
FlyOutWithin a component, we essentially have three things:
FlyOut- The wrapper, which contains the toggle button and the list
FlyOut - The button, which toggles the
ToggleList - The , which contains the list of menu items
List
Using the Compound component pattern with React's Context API is perfect for this example!
First, let's create the component. This component keeps the state, and returns a with the value of the toggle to all the children it receives.
FlyOutFlyOutProviderjs
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
);
}We now have a stateful component that can pass the value of and to its children!
FlyOutopentoggleLet's create the component. This component simply renders the component on which the user can click in order to toggle the menu.
Togglejs
function Toggle() {
const { open, toggle } = useContext(FlyOutContext);
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
);
}In order to actually give access to the provider, we need to render it as a child component of ! We can also make the component a property of the component!
ToggleFlyOutContextFlyOutToggleFlyOutjs
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;This means that if we ever want to use the component in any file, we only have to import !
FlyOutFlyOutjs
import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
</FlyOut>
);
}Just a toggle is not enough. We also need to have a with list items, which open and close based on the value of .
Listopenjs
function List({ children }) {
const { open } = React.useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function Item({ children }) {
return <li>{children}</li>;
}The component renders its children based on whether the value of is or . Let's make and a property of the component, just like we did with the component.
ListopentruefalseListItemFlyOutTogglejs
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;We can now use them as properties on the component! In this case, we want to show two options to the user: Edit and Delete. Let's create a that renders two components, one for the Edit option, and one for the Delete option.
FlyOutFlyOut.ListFlyOut.Itemjs
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! We just created an entire component without adding any state in the itself!
FlyOutFlyOutMenuThe compound pattern is great when you're building a component library. You'll often see this pattern when using UI libraries like Semantic UI.
我们来看一个示例:我们有一组松鼠图片!除了展示图片,我们还想添加一个按钮,让用户可以编辑或删除图片。我们可以实现一个组件,当用户切换时显示一个列表。
FlyOut在组件中,主要包含三部分:
FlyOut- 容器,包含切换按钮和列表
FlyOut - 按钮,用于切换
Toggle的显示状态List - ,包含菜单项列表
List
使用React的Context API结合复合组件模式非常适合这个场景!
首先,我们创建组件。该组件维护状态,并通过将切换状态值传递给所有子组件。
FlyOutFlyOutProviderjs
const FlyOutContext = createContext();
function FlyOut(props) {
const [open, toggle] = useState(false);
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
);
}现在我们有了一个有状态的组件,可以将和的值传递给它的子组件!
FlyOutopentoggle接下来创建组件。这个组件渲染一个用户可以点击的元素,用于切换菜单的显示状态。
Togglejs
function Toggle() {
const { open, toggle } = useContext(FlyOutContext);
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
);
}为了让能够访问提供的内容,我们需要将它作为的子组件渲染!我们还可以将组件设为组件的一个属性!
ToggleFlyOutContextFlyOutToggleFlyOutjs
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;这意味着如果我们想在任何文件中使用组件,只需要导入即可!
FlyOutFlyOutjs
import React from "react";
import { FlyOut } from "./FlyOut";
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
</FlyOut>
);
}只有切换按钮还不够,我们还需要一个组件,它会根据的值显示或关闭菜单项。
Listopenjs
function List({ children }) {
const { open } = React.useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function Item({ children }) {
return <li>{children}</li>;
}ListopentrueToggleListItemFlyOutjs
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;现在我们可以将它们作为组件的属性来使用了!在这个例子中,我们想给用户展示两个选项:编辑和删除。让我们创建一个,里面渲染两个组件,分别对应编辑和删除选项。
FlyOutFlyOut.ListFlyOut.Itemjs
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>
);
}完美!我们刚刚创建了一个完整的组件,而不需要在中管理任何状态!
FlyOutFlyOutMenu复合组件模式非常适合构建组件库。你在使用像Semantic UI这样的UI库时,经常会看到这种模式。
React.Children.map
React.Children.mapReact.Children.map
React.Children.mapWe can also implement the Compound Component pattern by mapping over the children of the component. We can add the and properties to these elements, by cloning them with the additional props.
opentogglejs
export function FlyOut(props) {
const [open, toggle] = React.useState(false);
return (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { open, toggle })
)}
</div>
);
}All children components are cloned, and passed the value of and . Instead of having to use the Context API like in the previous example, we now have access to these two values through .
opentoggleprops我们也可以通过遍历组件的子元素来实现复合组件模式。我们可以通过克隆元素并添加额外的props,将和属性添加到这些元素中。
opentogglejs
export function FlyOut(props) {
const [open, toggle] = React.useState(false);
return (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { open, toggle })
)}
</div>
);
}所有子组件都会被克隆,并传递和的值。与之前的例子中使用Context API不同,现在我们可以通过访问这两个值。
opentogglepropsPros
优点
Compound components manage their own internal state, which they share among the several child components. When implementing a compound component, we don't have to worry about managing the state ourselves.
When importing a compound component, we don't have to explicitly import the child components that are available on that component.
Note (React 18+): The compound component pattern using React's Context API remains a recommended pattern for related components that share state. The implementation using Hooks (,useState) is modern and aligns with current best practices. When using context, avoid unnecessary re-renders by not re-creating context values each render. In complex scenarios, you might optimize by memoizing the context value or splitting context (e.g., a context for theuseContextboolean and another for theopenfunction). The pattern is fully compatible with React's upcoming features like Server Components — just ensure the context provider and consumers are all either server or client components as needed.toggle
js
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 18+): 使用React Context API的复合组件模式仍然是推荐模式,适用于共享状态的关联组件。使用Hooks(、useState)的实现是现代化的,符合当前的最佳实践。使用Context时,避免每次渲染都重新创建Context值,以防止不必要的重渲染。在复杂场景中,你可以通过记忆化Context值或拆分Context(例如,一个用于useContext布尔值的Context,另一个用于open函数的Context)来进行优化。该模式完全兼容React即将推出的功能,如Server Components——只需确保Context提供者和消费者要么都是服务器组件,要么都是客户端组件即可。toggle
js
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>
);
}Cons
缺点
When using the to provide the values, the component nesting is limited. Only direct children of the parent component will have access to the and props, meaning we can't wrap any of these components in another component.
React.Children.mapopentogglejs
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>
);
}Cloning an element with performs a shallow merge. Already existing props will be merged together with the new props that we pass. This could end up in a naming collision, if an already existing prop has the same name as the props we're passing to the method. As the props are shallowly merged, the value of that prop will be overwritten with the latest value that we pass.
React.cloneElementReact.cloneElement当使用来传递值时,组件的嵌套会受到限制。只有父组件的直接子元素才能访问和 props,这意味着我们不能将这些组件包裹在另一个组件中。
React.Children.mapopentogglejs
export default function FlyoutMenu() {
return (
<FlyOut>
{/* 这样会失效 */}
<div>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</div>
</FlyOut>
);
}使用克隆元素时会进行浅合并。已有的props会与我们传递的新props合并。如果已有的props与我们传递给方法的props同名,可能会导致命名冲突。由于props是浅合并的,该props的值会被我们传递的最新值覆盖。
React.cloneElementReact.cloneElement