react-composition-2026

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Modern React Composition Patterns

现代React组合模式

Composition patterns for building flexible, maintainable React components that scale. These patterns replace boolean-prop proliferation, rigid component APIs, and tangled state with composable, explicit designs.
用于构建可扩展、灵活且易于维护的React组件的组合模式。这些模式通过可组合的明确设计,替代了大量布尔型props、僵化的组件API以及混乱的状态管理方式。

When to Use

适用场景

Reference these patterns when:
  • A component has more than 3-4 boolean props controlling its behavior
  • Building reusable UI components or a shared component library
  • Refactoring components that are difficult to extend
  • Designing component APIs that other teams will consume
  • Reviewing component architecture for flexibility and maintainability
在以下场景中可以参考这些模式:
  • 某个组件拥有3-4个以上控制其行为的布尔型props时
  • 构建可复用UI组件或共享组件库时
  • 重构难以扩展的组件时
  • 设计供其他团队使用的组件API时
  • 评审组件架构的灵活性与可维护性时

Instructions

使用说明

  • Apply these patterns during component design, code generation, and review. When you see boolean prop accumulation or rigid component APIs, suggest the appropriate composition pattern.
  • 在组件设计、代码生成和评审阶段应用这些模式。当你发现组件积累了大量布尔型props或API僵化时,建议采用合适的组合模式。

Details

详细内容

Overview

概述

The core principle: composition over configuration. Instead of adding boolean props and conditional branches to handle every variant, compose smaller, focused components together. This makes components easier to understand, test, and extend — for both humans and AI agents.

核心原则:组合优于配置。无需通过添加布尔型props和条件分支来处理所有变体,而是将更小、职责单一的组件组合在一起。这让组件对人类和AI Agent来说都更易于理解、测试和扩展。

1. Replace Boolean Props with Composition

1. 用组合替代布尔型Props

Impact: HIGH — Prevents combinatorial explosion and makes intent explicit.
Boolean props multiply complexity: 4 booleans = 16 possible states, most of which are untested. Replace them with composable children.
Avoid — boolean prop accumulation:
tsx
<Card
  showHeader
  showFooter
  collapsible
  bordered
  withShadow
  headerAction="close"
  size="large"
/>
Prefer — explicit composition:
tsx
<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>
Each piece is explicit, testable, and independently optional.

影响程度:高 —— 避免组合爆炸,让意图更明确。
布尔型props会成倍增加复杂度:4个布尔值就对应16种可能的状态,其中大多数都未经过测试。用可组合的子组件替代它们。
避免——积累大量布尔型props:
tsx
<Card
  showHeader
  showFooter
  collapsible
  bordered
  withShadow
  headerAction="close"
  size="large"
/>
推荐——明确的组合方式:
tsx
<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. Build Compound Components with Context

2. 结合Context构建复合组件

Impact: HIGH — Shared implicit state without prop drilling.
Compound components are a group of components that work together, sharing state through context rather than props. The parent owns the state; children consume it.
Avoid — parent manages everything through props:
tsx
<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}
/>
Prefer — compound components:
tsx
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
Usage:
tsx
<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>

影响程度:高 —— 无需prop透传即可共享隐式状态。
复合组件是一组协同工作的组件,它们通过Context而非props共享状态。父组件拥有状态,子组件消费状态。
避免——父组件通过props管理所有内容:
tsx
<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}
/>
推荐——复合组件:
tsx
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
使用示例:
tsx
<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. Create Explicit Variant Components

3. 创建明确的变体组件

Impact: MEDIUM — Makes each mode a clear, focused component.
When a component has distinct "modes" (dialog vs drawer, inline vs modal, card vs list-item), create explicit variant components instead of toggling with props.
Avoid — one component with mode props:
tsx
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} />
}
Prefer — explicit variants:
tsx
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} />
}
Each variant has exactly the props it needs — no impossible states, no unused props.

影响程度:中 —— 让每种模式都成为清晰、职责单一的组件。
当一个组件有多种不同“模式”(如对话框与抽屉、内联与模态框、卡片与列表项)时,创建明确的变体组件,而非通过props切换模式。
避免——单个组件通过props切换模式:
tsx
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} />
}
推荐——明确的变体组件:
tsx
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} />
}
每个变体组件只包含它需要的props——不存在不可能的状态,也没有未使用的props。

4. Use Children Over Render Props for Composition

4. 用Children而非Render Props实现组合

Impact: MEDIUM — Simpler API, better readability.
Render props (
renderHeader
,
renderItem
) were essential before hooks, but today
children
provides cleaner composition for most cases.
Avoid — render prop proliferation:
tsx
<DataTable
  data={users}
  renderHeader={() => <h2>Users</h2>}
  renderRow={(user) => <UserRow user={user} />}
  renderEmpty={() => <EmptyState />}
  renderFooter={() => <Pagination />}
/>
Prefer — children composition:
tsx
<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>
Reserve render props for cases where the parent needs to provide data to the renderer (e.g., virtualized list items).

影响程度:中 —— API更简洁,可读性更强。
在Hooks出现之前,Render Props(如
renderHeader
renderItem
)是必不可少的,但如今对于大多数场景,
children
能提供更简洁的组合方式。
避免——大量使用Render Props:
tsx
<DataTable
  data={users}
  renderHeader={() => <h2>Users</h2>}
  renderRow={(user) => <UserRow user={user} />}
  renderEmpty={() => <EmptyState />}
  renderFooter={() => <Pagination />}
/>
推荐——用Children实现组合:
tsx
<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. Decouple State Implementation from UI

5. 将状态实现与UI解耦

Impact: MEDIUM — Swap state management without changing components.
Define a generic interface for your state shape (value, actions, metadata), then let providers implement it. Components consume the interface, not the implementation.
Define the interface:
typescript
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
}
Implement with local state:
tsx
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>
}
Swap to API-backed state without changing consumers:
tsx
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>
}
The
useCounter()
consumers never change.

影响程度:中 —— 无需修改组件即可切换状态管理方式。
为状态形状(值、操作、元数据)定义通用接口,然后由提供者实现该接口。组件消费接口而非具体实现。
定义接口:
typescript
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
}
用本地状态实现:
tsx
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驱动的状态:
tsx
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. Lift State to Provider Components

6. 将状态提升至Provider组件

Impact: MEDIUM — Enables sibling communication without prop threading.
When two sibling components need shared state, lift it into a provider rather than threading callbacks through the parent.
Avoid — parent threads state to siblings:
tsx
function Page() {
  const [selected, setSelected] = useState<string | null>(null)
  return (
    <div>
      <Sidebar selected={selected} onSelect={setSelected} />
      <Detail selected={selected} />
    </div>
  )
}
Prefer — provider manages shared state:
tsx
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>
  )
}
Both
Sidebar
and
Detail
consume
useSelection()
directly.

影响程度:中 —— 无需prop透传即可实现兄弟组件通信。
当两个兄弟组件需要共享状态时,将状态提升至Provider中,而非通过父组件传递回调函数。
避免——父组件向兄弟组件传递状态:
tsx
function Page() {
  const [selected, setSelected] = useState<string | null>(null)
  return (
    <div>
      <Sidebar selected={selected} onSelect={setSelected} />
      <Detail selected={selected} />
    </div>
  )
}
推荐——由Provider管理共享状态:
tsx
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. Use Polymorphic
as
Props for Flexible Elements

7. 用多态
as
Props实现灵活元素

Impact: MEDIUM — One component, any underlying element or component.
The
as
prop pattern lets consumers control the rendered element while keeping your component's styles and behavior.
tsx
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>
}
Usage:
tsx
<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>

影响程度:中 —— 单个组件可对应任意底层元素或组件。
as
Prop模式允许使用者控制渲染的元素,同时保留组件的样式和行为。
tsx
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>
}
使用示例:
tsx
<Box>默认div</Box>
<Box as="section">一个section元素</Box>
<Box as="a" href="/about">一个链接</Box>
<Box as={Link} to="/about">路由链接</Box>

8. React 19: Drop
forwardRef
, Use
ref
as a Prop

8. React 19:弃用
forwardRef
,将
ref
作为普通Props使用

Impact: MEDIUM — Simpler component definitions.
React 19 passes
ref
as a regular prop. No more
forwardRef
wrapper.
React 18 (deprecated pattern):
tsx
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />
})
React 19:
tsx
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />
}
Similarly,
use()
can read either promises or context and can be called conditionally:
tsx
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>
}

影响程度:中 —— 组件定义更简洁。
在React 19中,
ref
作为普通Props传递,无需再使用
forwardRef
包装。
React 18(已弃用模式):
tsx
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />
})
React 19:
tsx
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />
}
同样,
use()
可以读取Promise或Context,并且可以被条件调用:
tsx
import { use } from 'react'

function Panel({ themePromise }: { themePromise: Promise<Theme> }) {
  const theme = use(themePromise)  // 解析Promise
  const user = use(UserContext)    // 条件读取Context
  return <div className={theme.bg}>{user.name}</div>
}

9. Slot Pattern for Layout Components

9. 布局组件的Slot模式

Impact: MEDIUM — Named insertion points without render props.
For layout components with multiple content areas, use a slot pattern based on child type detection or named sub-components.
tsx
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}</>
}
Usage:
tsx
<AppLayout>
  <AppLayout.Header>
    <Logo />
    <Nav />
  </AppLayout.Header>
  <Dashboard />
</AppLayout>

影响程度:中 —— 无需Render Props即可实现命名插入点。
对于拥有多个内容区域的布局组件,可基于子元素类型检测或命名子组件实现Slot模式。
tsx
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}</>
}
使用示例:
tsx
<AppLayout>
  <AppLayout.Header>
    <Logo />
    <Nav />
  </AppLayout.Header>
  <Dashboard />
</AppLayout>

10. Headless Components for Maximum Flexibility

10. 无头组件实现最大灵活性

Impact: HIGH — Logic without opinions about rendering.
Headless components provide behavior (state, keyboard handling, ARIA attributes) without any markup. Consumers supply the rendering.
tsx
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 }
}
Usage — consumer controls all rendering:
tsx
function DarkModeSwitch() {
  const { on, buttonProps } = useToggle(false)
  return (
    <button {...buttonProps} className={on ? 'dark' : 'light'}>
      {on ? 'Dark' : 'Light'} Mode
    </button>
  )
}
Libraries like Radix UI, Headless UI, and React Aria follow this pattern. Prefer them over fully-styled component libraries when you need design flexibility.

影响程度:高 —— 仅提供逻辑,不限制渲染方式。
无头组件提供行为(状态、键盘处理、ARIA属性)但不包含任何标记,由使用者负责渲染部分。
tsx
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 }
}
使用示例——使用者完全控制渲染:
tsx
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等库都遵循这种模式。当你需要设计灵活性时,优先选择这类库而非全样式组件库。

Source

来源

Patterns from patterns.dev — composition guidance for the broader React community.
这些模式来自patterns.dev——面向广大React社区的组合设计指南。