组合
构建现代 UI 组件的基础。
组合,或称可组合性,是构建现代 UI 组件的基础。这是创建灵活、可重用组件的最强大技术之一,能够在不牺牲 API 清晰度的情况下处理复杂需求。
与其将所有功能塞入一个具有数十个 props 的单一组件,不如通过多个协作组件来分配职责。
Fernando 在 React Universe Conf 2025 上对此做了很好的演讲,他分享了重建 Slack 的 Message Composer 作为可组合组件的方法。
使组件可组合
要使组件可组合,你需要将其拆分为更小、更专注的组件。例如,让我们看一下这个 Accordion 组件:
import { Accordion } from '@/components/ui/accordion';
const data = [
{
title: 'Accordion 1',
content: 'Accordion 1 content',
},
{
title: 'Accordion 2',
content: 'Accordion 2 content',
},
{
title: 'Accordion 3',
content: 'Accordion 3 content',
},
];
return (
<Accordion data={data} />
);虽然这个 Accordion 组件看起来很简单,但它承担了过多职责。它负责渲染容器、trigger 和 content;同时还负责处理手风琴的状态和数据。
定制此组件的样式很困难,因为它耦合性很强。很可能需要全局 CSS 覆盖。此外,添加新功能或调整行为需要修改组件源代码。
为了解决这个问题,我们可以把它拆分为更小、更专注的组件。
1. Root 组件
首先,我们关注容器——即将所有内容(trigger 和 content)包裹在一起的组件。这个容器不需要了解数据,但需要跟踪 open 状态。
不过,我们也希望子组件可以访问到这个状态。因此,我们使用 Context API 来为 open 状态创建一个上下文。
最后,为了允许修改 div 元素,我们将扩展默认的 HTML 属性。
我们把这个组件称为 "Root" 组件。
type AccordionProps = React.ComponentProps<'div'> & {
open: boolean;
setOpen: (open: boolean) => void;
};
const AccordionContext = createContext<AccordionProps>({
open: false,
setOpen: () => {},
});
export type AccordionRootProps = React.ComponentProps<'div'> & {
open: boolean;
setOpen: (open: boolean) => void;
};
export const Root = ({
children,
open,
setOpen,
...props
}: AccordionRootProps) => (
<AccordionContext.Provider value={{ open, setOpen }}>
<div {...props}>{children}</div>
</AccordionContext.Provider>
);2. Item 组件
Item 组件是包含手风琴项的元素。它只是手风琴中每个项的包装器。
export type AccordionItemProps = React.ComponentProps<'div'>;
export const Item = (props: AccordionItemProps) => <div {...props} />;3. Trigger 组件
Trigger 组件是激活时打开手风琴的元素。它负责:
- 默认以按钮渲染(可以通过
asChild自定义) - 处理点击事件以打开手风琴
- 在手风琴关闭时管理焦点
- 提供正确的 ARIA 属性
让我们把这个组件添加到我们的 Accordion 组件中。
export type AccordionTriggerProps = React.ComponentProps<'button'> & {
asChild?: boolean;
};
export const Trigger = ({ asChild, ...props }: AccordionTriggerProps) => (
<AccordionContext.Consumer>
{({ open, setOpen }) => (
<button onClick={() => setOpen(!open)} {...props} />
)}
</AccordionContext.Consumer>
);4. Content 组件
Content 组件是包含手风琴内容的元素。它负责:
- 在手风琴打开时渲染内容
- 提供正确的 ARIA 属性
让我们把这个组件添加到我们的 Accordion 组件中。
export type AccordionContentProps = React.ComponentProps<'div'> & {
asChild?: boolean;
};
export const Content = ({ asChild, ...props }: AccordionContentProps) => (
<AccordionContext.Consumer>
{({ open }) => <div {...props} />}
</AccordionContext.Consumer>
);5. 将它们组合在一起
现在我们有了所有子组件,可以把它们放回到原始文件中。
import * as Accordion from '@/components/ui/accordion';
const data = [
{
title: 'Accordion 1',
content: 'Accordion 1 content',
},
{
title: 'Accordion 2',
content: 'Accordion 2 content',
},
{
title: 'Accordion 3',
content: 'Accordion 3 content',
},
];
return (
<Accordion.Root open={false} setOpen={() => {}}>
{data.map((item) => (
<Accordion.Item key={item.title}>
<Accordion.Trigger>{item.title}</Accordion.Trigger>
<Accordion.Content>{item.content}</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
);命名约定
在构建可组合组件时,一致的命名约定对于创建直观且可预测的 API 至关重要。shadcn/ui 和 Radix UI 都遵循已成为 React 生态中事实标准的既定模式。
Root 组件
Root 组件作为包装所有其他子组件的主容器。它通常通过提供上下文来管理共享状态和子组件的上下文。
<AccordionRoot>{/* Child components */}</AccordionRoot>交互元素
触发操作或切换状态的交互组件使用描述性名称:
Trigger- 发起操作的元素(打开、关闭、切换)Content- 包含被显示/隐藏的主要内容的元素
<CollapsibleTrigger>Click to expand</CollapsibleTrigger>
<CollapsibleContent>
Hidden content revealed here
</CollapsibleContent>内容结构
对于具有结构化内容区域的组件,使用能描述其用途的语义名称:
Header- 顶部区域,包含标题或控件Body- 主要内容区域Footer- 底部区域,用于操作或元数据
<DialogHeader>
{/* Dialog title */}
</DialogHeader>
<DialogBody>
{/* Dialog content */}
</DialogBody>
<DialogFooter>
{/* Dialog footer */}
</DialogFooter>信息性组件
提供信息或上下文的组件使用描述性后缀:
Title- 主要标题或标签Description- 辅助文本或说明性内容
<CardTitle>Project Statistics</CardTitle>
<CardDescription>
View your project's performance over time
</CardDescription>