React 组合模式(React Composition Patterns)

Verified 高级 Advanced 参考型 Reference ⚡ Claude Code 专属 ⚡ Claude Code Optimized
17 min read · 831 lines

Vercel 官方组合模式:复合组件 + 状态提升 + 泛型 Context 依赖注入

React 组合模式(React Composition Patterns)

概述

用于构建灵活、可维护的 React 组件的组合模式。通过使用复合组件(compound components)、状态提升(lifting state)和组合内部构件(composing internals),避免布尔属性(boolean props)泛滥。这些模式使代码库在规模扩大时对人类和 AI Agent 都更易于使用。

适用场景

  • 重构拥有大量布尔属性的组件
  • 构建可复用的组件库
  • 设计灵活的组件 API
  • 审查组件架构
  • 使用复合组件或 Context Provider

规则类别优先级

优先级 类别 影响 前缀
1 组件架构 architecture-
2 状态管理 state-
3 实现模式 patterns-
4 React 19 API react19-

1. 组件架构(Component Architecture)

影响:高

构建组件以避免属性泛滥并启用灵活组合的基本模式。

1.1 避免布尔属性泛滥(Avoid Boolean Prop Proliferation)

影响:关键(防止不可维护的组件变体)

不要添加 isThreadisEditingisDMThread 等布尔属性来定制组件行为。每个布尔值都会使可能的状态翻倍,并创造不可维护的条件逻辑。应使用组合(composition)代替。

错误:布尔属性创造指数级复杂度

function Composer({
  onSubmit,
  isThread,
  channelId,
  isDMThread,
  dmId,
  isEditing,
  isForwarding,
}: Props) {
  return (
    <form>
      <Header />
      <Input />
      {isDMThread ? (
        <AlsoSendToDMField id={dmId} />
      ) : isThread ? (
        <AlsoSendToChannelField id={channelId} />
      ) : null}
      {isEditing ? (
        <EditActions />
      ) : isForwarding ? (
        <ForwardActions />
      ) : (
        <DefaultActions />
      )}
      <Footer onSubmit={onSubmit} />
    </form>
  )
}

正确:组合消除条件语句

// 频道编辑器
function ChannelComposer() {
  return (
    <Composer.Frame>
      <Composer.Header />
      <Composer.Input />
      <Composer.Footer>
        <Composer.Attachments />
        <Composer.Formatting />
        <Composer.Emojis />
        <Composer.Submit />
      </Composer.Footer>
    </Composer.Frame>
  )
}

// 线程编辑器——添加"同时发送到频道"字段
function ThreadComposer({ channelId }: { channelId: string }) {
  return (
    <Composer.Frame>
      <Composer.Header />
      <Composer.Input />
      <AlsoSendToChannelField id={channelId} />
      <Composer.Footer>
        <Composer.Formatting />
        <Composer.Emojis />
        <Composer.Submit />
      </Composer.Footer>
    </Composer.Frame>
  )
}

// 编辑编辑器——不同的底部操作
function EditComposer() {
  return (
    <Composer.Frame>
      <Composer.Input />
      <Composer.Footer>
        <Composer.Formatting />
        <Composer.Emojis />
        <Composer.CancelEdit />
        <Composer.SaveEdit />
      </Composer.Footer>
    </Composer.Frame>
  )
}

每个变体都明确说明了它渲染的内容。我们可以共享内部构件而不共享单一的整体父组件。

1.2 使用复合组件(Use Compound Components)

影响:高(无需属性传递即可实现灵活组合)

将复杂组件结构化为共享上下文的复合组件。每个子组件通过 context 而非 props 访问共享状态。消费者可以按需组合所需的部分。

错误:带有 render props 的整体式组件

function Composer({
  renderHeader,
  renderFooter,
  renderActions,
  showAttachments,
  showFormatting,
  showEmojis,
}: Props) {
  return (
    <form>
      {renderHeader?.()}
      <Input />
      {showAttachments && <Attachments />}
      {renderFooter ? (
        renderFooter()
      ) : (
        <Footer>
          {showFormatting && <Formatting />}
          {showEmojis && <Emojis />}
          {renderActions?.()}
        </Footer>
      )}
    </form>
  )
}

正确:带有共享上下文的复合组件

const ComposerContext = createContext<ComposerContextValue | null>(null)

function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
  return (
    <ComposerContext value={{ state, actions, meta }}>
      {children}
    </ComposerContext>
  )
}

function ComposerFrame({ children }: { children: React.ReactNode }) {
  return <form>{children}</form>
}

function ComposerInput() {
  const {
    state,
    actions: { update },
    meta: { inputRef },
  } = use(ComposerContext)
  return (
    <TextInput
      ref={inputRef}
      value={state.input}
      onChangeText={(text) => update((s) => ({ ...s, input: text }))}
    />
  )
}

function ComposerSubmit() {
  const {
    actions: { submit },
  } = use(ComposerContext)
  return <Button onPress={submit}>Send</Button>
}

// 作为复合组件导出
const Composer = {
  Provider: ComposerProvider,
  Frame: ComposerFrame,
  Input: ComposerInput,
  Submit: ComposerSubmit,
  Header: ComposerHeader,
  Footer: ComposerFooter,
  Attachments: ComposerAttachments,
  Formatting: ComposerFormatting,
  Emojis: ComposerEmojis,
}

使用方式:

<Composer.Provider state={state} actions={actions} meta={meta}>
  <Composer.Frame>
    <Composer.Header />
    <Composer.Input />
    <Composer.Footer>
      <Composer.Formatting />
      <Composer.Submit />
    </Composer.Footer>
  </Composer.Frame>
</Composer.Provider>

消费者明确组合他们需要的内容。没有隐藏的条件语句。状态、actions 和 meta 由父级 provider 进行依赖注入。


2. 状态管理(State Management)

影响:中

用于提升状态和管理组合组件间共享上下文的模式。

2.1 将状态管理与 UI 解耦(Decouple State Management from UI)

影响:中(在不改变 UI 的情况下可替换状态实现)

Provider 组件应该是唯一知道状态如何管理的地方。UI 组件消费 context 接口——它们不知道状态来自 useState、Zustand 还是服务器同步。

错误:UI 耦合到状态实现

function ChannelComposer({ channelId }: { channelId: string }) {
  // UI 组件知道全局状态的实现细节
  const state = useGlobalChannelState(channelId)
  const { submit, updateInput } = useChannelSync(channelId)

  return (
    <Composer.Frame>
      <Composer.Input
        value={state.input}
        onChange={(text) => sync.updateInput(text)}
      />
      <Composer.Submit onPress={() => sync.submit()} />
    </Composer.Frame>
  )
}

正确:状态管理隔离在 provider 中

// Provider 处理所有状态管理细节
function ChannelProvider({
  channelId,
  children,
}: {
  channelId: string
  children: React.ReactNode
}) {
  const { state, update, submit } = useGlobalChannel(channelId)
  const inputRef = useRef(null)

  return (
    <Composer.Provider
      state={state}
      actions={{ update, submit }}
      meta={{ inputRef }}
    >
      {children}
    </Composer.Provider>
  )
}

// UI 组件只知道 context 接口
function ChannelComposer() {
  return (
    <Composer.Frame>
      <Composer.Header />
      <Composer.Input />
      <Composer.Footer>
        <Composer.Submit />
      </Composer.Footer>
    </Composer.Frame>
  )
}

// 使用方式
function Channel({ channelId }: { channelId: string }) {
  return (
    <ChannelProvider channelId={channelId}>
      <ChannelComposer />
    </ChannelProvider>
  )
}

不同的 providers,相同的 UI:

// 临时表单的本地状态
function ForwardMessageProvider({ children }) {
  const [state, setState] = useState(initialState)
  const forwardMessage = useForwardMessage()

  return (
    <Composer.Provider
      state={state}
      actions={{ update: setState, submit: forwardMessage }}
    >
      {children}
    </Composer.Provider>
  )
}

// 频道的全局同步状态
function ChannelProvider({ channelId, children }) {
  const { state, update, submit } = useGlobalChannel(channelId)

  return (
    <Composer.Provider state={state} actions={{ update, submit }}>
      {children}
    </Composer.Provider>
  )
}

同一个 Composer.Input 组件可以与两个 providers 一起工作,因为它只依赖 context 接口,而非实现。

2.2 定义泛型 Context 接口进行依赖注入

影响:高(在不同使用场景中实现依赖注入式状态)

为你的组件 context 定义一个包含三部分的泛型接口stateactionsmeta。这个接口是任何 provider 都可以实现的契约——使同样的 UI 组件可以与完全不同的状态实现配合工作。

核心原则: 状态提升、组合内部构件、使状态可依赖注入。

错误:UI 耦合到特定状态实现

function ComposerInput() {
  // 紧密耦合到特定的 hook
  const { input, setInput } = useChannelComposerState()
  return <TextInput value={input} onChangeText={setInput} />
}

正确:泛型接口启用依赖注入

// 定义任何 provider 都可以实现的泛型接口
interface ComposerState {
  input: string
  attachments: Attachment[]
  isSubmitting: boolean
}

interface ComposerActions {
  update: (updater: (state: ComposerState) => ComposerState) => void
  submit: () => void
}

interface ComposerMeta {
  inputRef: React.RefObject<TextInput>
}

interface ComposerContextValue {
  state: ComposerState
  actions: ComposerActions
  meta: ComposerMeta
}

const ComposerContext = createContext<ComposerContextValue | null>(null)

UI 组件消费接口,而非实现:

function ComposerInput() {
  const {
    state,
    actions: { update },
    meta,
  } = use(ComposerContext)

  // 这个组件可以与任何实现了接口的 provider 配合工作
  return (
    <TextInput
      ref={meta.inputRef}
      value={state.input}
      onChangeText={(text) => update((s) => ({ ...s, input: text }))}
    />
  )
}

不同的 providers 实现相同的接口:

// Provider A:临时表单的本地状态
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState(initialState)
  const inputRef = useRef(null)
  const submit = useForwardMessage()

  return (
    <ComposerContext
      value={{
        state,
        actions: { update: setState, submit },
        meta: { inputRef },
      }}
    >
      {children}
    </ComposerContext>
  )
}

// Provider B:频道的全局同步状态
function ChannelProvider({ channelId, children }: Props) {
  const { state, update, submit } = useGlobalChannel(channelId)
  const inputRef = useRef(null)

  return (
    <ComposerContext
      value={{
        state,
        actions: { update, submit },
        meta: { inputRef },
      }}
    >
      {children}
    </ComposerContext>
  )
}

Provider 之外的自定义 UI 也可以访问状态和 actions:

function ForwardMessageDialog() {
  return (
    <ForwardMessageProvider>
      <Dialog>
        {/* 编辑器 UI */}
        <Composer.Frame>
          <Composer.Input placeholder="Add a message, if you'd like." />
          <Composer.Footer>
            <Composer.Formatting />
            <Composer.Emojis />
          </Composer.Footer>
        </Composer.Frame>

        {/* 编辑器外部但在 provider 内部的自定义 UI */}
        <MessagePreview />

        {/* 对话框底部的操作 */}
        <DialogActions>
          <CancelButton />
          <ForwardButton />
        </DialogActions>
      </Dialog>
    </ForwardMessageProvider>
  )
}

// 这个按钮在 Composer.Frame 外部但仍可以根据 context 提交!
function ForwardButton() {
  const {
    actions: { submit },
  } = use(ComposerContext)
  return <Button onPress={submit}>Forward</Button>
}

// 这个预览在 Composer.Frame 外部但可以读取编辑器的状态!
function MessagePreview() {
  const { state } = use(ComposerContext)
  return <Preview message={state.input} attachments={state.attachments} />
}

重要的是 provider 边界——而非视觉嵌套。需要共享状态的组件不必在 Composer.Frame 内部。它们只需在 provider 内部。

2.3 将状态提升到 Provider 组件中(Lift State into Provider Components)

影响:高(实现组件边界外的状态共享)

将状态管理移到专门的 provider 组件中。这允许主 UI 外部的兄弟组件在不进行属性传递或使用 refs 的情况下访问和修改状态。

错误:状态困在组件内部

function ForwardMessageComposer() {
  const [state, setState] = useState(initialState)
  const forwardMessage = useForwardMessage()

  return (
    <Composer.Frame>
      <Composer.Input />
      <Composer.Footer />
    </Composer.Frame>
  )
}

// 问题:这个按钮如何访问编辑器状态?
function ForwardMessageDialog() {
  return (
    <Dialog>
      <ForwardMessageComposer />
      <MessagePreview /> {/* 需要编辑器状态 */}
      <DialogActions>
        <CancelButton />
        <ForwardButton /> {/* 需要调用 submit */}
      </DialogActions>
    </Dialog>
  )
}

错误:使用 useEffect 同步状态向上

function ForwardMessageDialog() {
  const [input, setInput] = useState('')
  return (
    <Dialog>
      <ForwardMessageComposer onInputChange={setInput} />
      <MessagePreview input={input} />
    </Dialog>
  )
}

function ForwardMessageComposer({ onInputChange }) {
  const [state, setState] = useState(initialState)
  useEffect(() => {
    onInputChange(state.input) // 每次变化都同步
  }, [state.input])
}

正确:状态提升到 provider

function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState(initialState)
  const forwardMessage = useForwardMessage()
  const inputRef = useRef(null)

  return (
    <Composer.Provider
      state={state}
      actions={{ update: setState, submit: forwardMessage }}
      meta={{ inputRef }}
    >
      {children}
    </Composer.Provider>
  )
}

function ForwardMessageDialog() {
  return (
    <ForwardMessageProvider>
      <Dialog>
        <ForwardMessageComposer />
        <MessagePreview /> {/* 自定义组件可以访问状态和 actions */}
        <DialogActions>
          <CancelButton />
          <ForwardButton /> {/* 自定义组件可以访问状态和 actions */}
        </DialogActions>
      </Dialog>
    </ForwardMessageProvider>
  )
}

function ForwardButton() {
  const { actions } = use(Composer.Context)
  return <Button onPress={actions.submit}>Forward</Button>
}

关键洞察: 需要共享状态的组件不必在视觉上嵌套在彼此内部——它们只需在同一个 provider 内部。


3. 实现模式(Implementation Patterns)

影响:中

实现复合组件和 context providers 的具体技术。

3.1 创建显式组件变体(Create Explicit Component Variants)

影响:中(自文档化的代码,没有隐藏的条件语句)

不要用一个组件加很多布尔属性,而是创建显式的变体组件。每个变体组合它需要的部分。代码自己说明自己。

错误:一个组件,多种模式

// 这个组件到底渲染什么?
<Composer
  isThread
  isEditing={false}
  channelId='abc'
  showAttachments
  showFormatting={false}
/>

正确:显式变体

// 立即清楚这渲染什么
<ThreadComposer channelId="abc" />

// 或者
<EditMessageComposer messageId="xyz" />

// 或者
<ForwardMessageComposer messageId="123" />

实现:

function ThreadComposer({ channelId }: { channelId: string }) {
  return (
    <ThreadProvider channelId={channelId}>
      <Composer.Frame>
        <Composer.Input />
        <AlsoSendToChannelField channelId={channelId} />
        <Composer.Footer>
          <Composer.Formatting />
          <Composer.Emojis />
          <Composer.Submit />
        </Composer.Footer>
      </Composer.Frame>
    </ThreadProvider>
  )
}

function EditMessageComposer({ messageId }: { messageId: string }) {
  return (
    <EditMessageProvider messageId={messageId}>
      <Composer.Frame>
        <Composer.Input />
        <Composer.Footer>
          <Composer.Formatting />
          <Composer.Emojis />
          <Composer.CancelEdit />
          <Composer.SaveEdit />
        </Composer.Footer>
      </Composer.Frame>
    </EditMessageProvider>
  )
}

function ForwardMessageComposer({ messageId }: { messageId: string }) {
  return (
    <ForwardMessageProvider messageId={messageId}>
      <Composer.Frame>
        <Composer.Input placeholder="Add a message, if you'd like." />
        <Composer.Footer>
          <Composer.Formatting />
          <Composer.Emojis />
          <Composer.Mentions />
        </Composer.Footer>
      </Composer.Frame>
    </ForwardMessageProvider>
  )
}

每个变体明确说明:使用什么 provider/状态、包含什么 UI 元素、有什么操作可用。没有布尔属性组合需要推理。没有不可能的状态。

3.2 优先使用 Children 组合而非 Render Props

影响:中(更清晰的组合,更好的可读性)

使用 children 进行组合而非 renderX 属性。Children 更可读,自然组合,不需要理解回调签名。

错误:render props

function Composer({
  renderHeader,
  renderFooter,
  renderActions,
}: {
  renderHeader?: () => React.ReactNode
  renderFooter?: () => React.ReactNode
  renderActions?: () => React.ReactNode
}) {
  return (
    <form>
      {renderHeader?.()}
      <Input />
      {renderFooter ? renderFooter() : <DefaultFooter />}
      {renderActions?.()}
    </form>
  )
}

// 使用方式笨拙且不灵活
return (
  <Composer
    renderHeader={() => <CustomHeader />}
    renderFooter={() => (
      <>
        <Formatting />
        <Emojis />
      </>
    )}
    renderActions={() => <SubmitButton />}
  />
)

正确:带有 children 的复合组件

function ComposerFrame({ children }: { children: React.ReactNode }) {
  return <form>{children}</form>
}

function ComposerFooter({ children }: { children: React.ReactNode }) {
  return <footer className='flex'>{children}</footer>
}

// 使用方式灵活
return (
  <Composer.Frame>
    <CustomHeader />
    <Composer.Input />
    <Composer.Footer>
      <Composer.Formatting />
      <Composer.Emojis />
      <SubmitButton />
    </Composer.Footer>
  </Composer.Frame>
)

适合使用 render props 的场景:

// 当你需要将数据传回时,render props 效果更好
<List
  data={items}
  renderItem={({ item, index }) => <Item item={item} index={index} />}
/>

当父级需要向子级提供数据或状态时使用 render props。当组合静态结构时使用 children。


4. React 19 API

影响:中

仅限 React 19+。不再使用 forwardRef;使用 use() 替代 useContext()

4.1 React 19 API 变更

影响:中(更简洁的组件定义和 context 使用)

注意:仅限 React 19+。 如果你使用 React 18 或更早版本,请跳过此部分。

在 React 19 中,ref 现在是一个普通的 prop(不需要 forwardRef 包装器),use() 替代了 useContext()

错误:React 19 中使用 forwardRef

const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
  return <TextInput ref={ref} {...props} />
})

正确:ref 作为普通 prop

function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
  return <TextInput ref={ref} {...props} />
}

错误:React 19 中使用 useContext

const value = useContext(MyContext)

正确:使用 use 替代 useContext

const value = use(MyContext)

use()useContext() 不同,可以在条件语句中调用。


参考资料

  1. https://react.dev
  2. https://react.dev/learn/passing-data-deeply-with-context
  3. https://react.dev/reference/react/use

相关技能 Related Skills