关于 react-composition-2026
面向 React 开发者的组件组合模式指南,适用于设计、重构和评审灵活可维护的组件 API。内容涵盖复合组件、显式变体、状态解耦等模式,也包括 React 19 的 ref 用法。
主要功能
- 01用组合替代布尔属性堆叠
- 02通过 Context 构建复合组件
- 03使用明确的变体组件
- 04以 children 进行组合
- 05将状态实现与 UI 解耦
- 06介绍多态、插槽和无头组件
适用场景
- 01重构难以扩展的组件
- 02设计可复用组件库 API
- 03评审布尔属性过多的组件
- 04让共享组件状态实现可替换
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill react-composition-2026安装后在 Claude Code 中输入/react-composition-2026确认已加载。
详细介绍
现代 React 组合模式
目录
用于构建灵活、易维护且可扩展的 React 组件的组合模式。这些模式以可组合、明确的设计取代布尔属性泛滥、僵化的组件 API 和混乱的状态。
适用场景
在以下情况下参考这些模式:
- 组件有超过 3-4 个用于控制行为的布尔属性
- 构建可复用的 UI 组件或共享组件库
- 重构难以扩展的组件
- 设计供其他团队使用的组件 API
- 审查组件架构的灵活性和可维护性
说明
- 在组件设计、代码生成和审查过程中应用这些模式。发现布尔属性不断累积或组件 API 僵化时,建议采用适当的组合模式。
详情
概览
核心原则:组合优于配置。不要通过添加布尔属性和条件分支来处理每种变体,而是将更小、更专注的组件组合起来。这样能让组件更易理解、测试和扩展——无论是对人类还是 AI Agent 都如此。
1. 用组合取代布尔属性
影响:高——防止组合爆炸,让意图更加明确。
布尔属性会成倍增加复杂性:4 个布尔值就有 16 种可能状态,其中大多数都未经测试。用可组合的子组件取代它们。
避免——布尔属性不断累积:
<Card
showHeader
showFooter
collapsible
bordered
withShadow
headerAction="close"
size="large"
/>
推荐——明确组合:
<Card variant="bordered" shadow="md">
<Card.Header>
<h3>Title</h3>
<Card.CloseButton />
</Card.Header>
<Card.Body collapsible>
<p>Content here</p>
</Card.Body>
<Card.Footer>
<Button>Save</Button>
</Card.Footer>
</Card>
每个部分都明确、可测试,并且可以独立选择是否使用。
2. 使用 Context 构建复合组件
影响:高——共享隐式状态,无需逐层传递属性。
复合组件是一组协同工作的组件,它们通过 context 而非属性共享状态。父组件拥有状态,子组件读取状态。
避免——父组件通过属性管理所有内容:
<Select
options={options}
value={value}
onChange={onChange}
renderOption={(opt) => <span>{opt.icon} {opt.label}</span>}
renderSelected={(opt) => <b>{opt.label}</b>}
placeholder="Choose..."
clearable
searchable
maxHeight={300}
/>
推荐——复合组件:
const SelectContext = createContext<SelectState | null>(null)
function Select({ children, value, onChange }: SelectProps) {
const [open, setOpen] = useState(false)
const ctx = useMemo(() => ({ value, onChange, open, setOpen }), [value, onChange, open])
return (
<SelectContext.Provider value={ctx}>
<div className="select-root">{children}</div>
</SelectContext.Provider>
)
}
function Trigger({ children }: { children: React.ReactNode }) {
const { open, setOpen } = useSelectContext()
return <button onClick={() => setOpen(!open)}>{children}</button>
}
function Options({ children }: { children: React.ReactNode }) {
const { open } = useSelectContext()
if (!open) return null
return <ul role="listbox">{children}</ul>
}
function Option({ value, children }: OptionProps) {
const { value: selected, onChange, setOpen } = useSelectContext()
return (
<li
role="option"
aria-selected={value === selected}
onClick={() => { onChange(value); setOpen(false) }}
>
{children}
</li>
)
}
Select.Trigger = Trigger
Select.Options = Options
Select.Option = Option
用法:
<Select value={color} onChange={setColor}>
<Select.Trigger>Pick a color</Select.Trigger>
<Select.Options>
<Select.Option value="red">Red</Select.Option>
<Select.Option value="blue">Blue</Select.Option>
</Select.Options>
</Select>
3. 创建明确的变体组件
影响:中——让每种模式都成为清晰、专注的组件。
当组件有不同的“模式”(对话框与抽屉、内嵌与模态、卡片与列表项)时,应创建明确的变体组件,而不是通过属性切换。
避免——用一个组件和模式属性处理所有情况:
function MediaDisplay({ type, src, title, showControls, autoPlay, loop }: Props) {
if (type === 'video') {
return <video src={src} controls={showControls} autoPlay={autoPlay} loop={loop} />
}
if (type === 'audio') {
return <audio src={src} controls={showControls} />
}
return <img src={src} alt={title} />
}
推荐——明确的变体:
function VideoPlayer({ src, controls, autoPlay, loop }: VideoProps) {
return <video src={src} controls={controls} autoPlay={autoPlay} loop={loop} />
}
function AudioPlayer({ src, controls }: AudioProps) {
return <audio src={src} controls={controls} />
}
function Image({ src, alt }: ImageProps) {
return <img src={src} alt={alt} />
}
每个变体只包含所需的属性——没有不可能的状态,也没有未使用的属性。
4. 组合时优先使用 Children,而非 Render Props
影响:中——API 更简单,可读性更好。
在 Hooks 出现之前,Render Props(renderHeader、renderItem)不可或缺;但如今大多数情况下,使用 children 进行组合更加简洁。
避免——Render Props 不断累积:
<DataTable
data={users}
renderHeader={() => <h2>Users</h2>}
renderRow={(user) => <UserRow user={user} />}
renderEmpty={() => <EmptyState />}
renderFooter={() => <Pagination />}
/>
推荐——使用 Children 组合:
<DataTable data={users}>
<DataTable.Header>
<h2>Users</h2>
</DataTable.Header>
<DataTable.Body>
{users.map(user => <UserRow key={user.id} user={user} />)}
</DataTable.Body>
<DataTable.Empty>
<EmptyState />
</DataTable.Empty>
<DataTable.Footer>
<Pagination />
</DataTable.Footer>
</DataTable>
仅在父组件需要向渲染器提供数据时使用 Render Props(例如虚拟列表项)。
5. 将状态实现与 UI 解耦
影响:中——更换状态管理方式,无需更改组件。
为状态结构(值、操作、元数据)定义通用接口,然后由 Provider 实现该接口。组件使用接口,而非具体实现。
定义接口:
interface CounterState {
count: number
increment: () => void
decrement: () => void
isLoading: boolean
}
const CounterContext = createContext<CounterState | null>(null)
function useCounter() {
const ctx = useContext(CounterContext)
if (!ctx) throw new Error('useCounter must be used within a CounterProvider')
return ctx
}
使用本地状态实现:
function LocalCounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0)
const value = useMemo(() => ({
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
isLoading: false,
}), [count])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}
切换为由 API 支持的状态实现,无需更改使用方:
function ApiCounterProvider({ children }: { children: React.ReactNode }) {
const { data, mutate } = useSWR('/api/counter', fetcher)
const value = useMemo(() => ({
count: data?.count ?? 0,
increment: () => mutate(patch('/api/counter', { delta: 1 })),
decrement: () => mutate(patch('/api/counter', { delta: -1 })),
isLoading: !data,
}), [data, mutate])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}
使用 useCounter() 的组件无需更改。
6. 将状态提升到 Provider 组件
影响:中——无需逐层传递属性,即可实现兄弟组件间的通信。
当两个兄弟组件需要共享状态时,将状态提升到 Provider 中,而不是通过父组件逐层传递回调。
避免——父组件向兄弟组件逐层传递状态:
function Page() {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</div>
)
}
推荐——Provider 管理共享状态:
function SelectionProvider({ children }: { children: React.ReactNode }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<SelectionContext.Provider value={{ selected, setSelected }}>
{children}
</SelectionContext.Provider>
)
}
function Page() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
)
}
Sidebar 和 Detail 都直接使用 useSelection()。
7. 使用多态 as 属性实现灵活元素
影响:中 — 一个组件,可对应任意底层元素或组件。
as 属性模式允许使用者控制渲染的元素,同时保留组件的样式和行为。
type BoxProps<C extends React.ElementType = 'div'> = {
as?: C
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<C>, 'as' | 'children'>
function Box<C extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<C>) {
const Component = as || 'div'
return <Component {...props}>{children}</Component>
}
用法:
<Box>Default div</Box>
<Box as="section">A section</Box>
<Box as="a" href="/about">A link</Box>
<Box as={Link} to="/about">Router link</Box>
8. React 19:移除 forwardRef,将 ref 用作属性
影响:中 — 组件定义更简单。
React 19 将 ref 作为常规属性传递。不再需要 forwardRef 包装器。
React 18(已弃用的模式):
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />
})
React 19:
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />
}
同样,use() 可以读取 Promise 或 context,并且可以有条件地调用:
import { use } from 'react'
function Panel({ themePromise }: { themePromise: Promise<Theme> }) {
const theme = use(themePromise) // unwraps promise
const user = use(UserContext) // conditional context read
return <div className={theme.bg}>{user.name}</div>
}
9. 布局组件的插槽模式
影响:中 — 无需渲染属性即可设置命名插入点。
对于包含多个内容区域的布局组件,请使用基于子元素类型检测或命名子组件的插槽模式。
function AppLayout({ children }: { children: React.ReactNode }) {
const slots = React.Children.toArray(children)
const header = slots.find(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === AppLayout.Header
)
const content = slots.filter(
(child) => !React.isValidElement(child) || child.type !== AppLayout.Header
)
return (
<div className="app-layout">
<header>{header}</header>
<main>{content}</main>
</div>
)
}
AppLayout.Header = function Header({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
用法:
<AppLayout>
<AppLayout.Header>
<Logo />
<Nav />
</AppLayout.Header>
<Dashboard />
</AppLayout>
10. 无头组件,实现最大灵活性
影响:高 — 只提供逻辑,不限定渲染方式。
无头组件提供行为(状态、键盘处理、ARIA 属性),但不包含任何标记。由使用者提供渲染方式。
function useToggle(initial = false) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(o => !o), [])
const buttonProps = {
'aria-pressed': on,
onClick: toggle,
role: 'switch' as const,
}
return { on, toggle, buttonProps }
}
用法 — 使用者控制所有渲染:
function DarkModeSwitch() {
const { on, buttonProps } = useToggle(false)
return (
<button {...buttonProps} className={on ? 'dark' : 'light'}>
{on ? 'Dark' : 'Light'} Mode
</button>
)
}
Radix UI、Headless UI 和 React Aria 等库都采用这种模式。当你需要灵活设计时,优先选择它们,而不是完全预设样式的组件库。
来源
这些模式来自 patterns.dev — 为更广泛的 React 社区提供组合指导。