首页
/ Supabase 仓库实战:React 组件组合模式——为什么 children 组合优于 render props

Supabase 仓库实战:React 组件组合模式——为什么 children 组合优于 render props

2026-09-06 20:42:03作者:何将鹤

本文基于 Supabase 仓库内置的 Vercel Composition Patterns 技能规则 patterns-children-over-render-props.md,系统讲解 React 组件 API 设计中的一条核心准则:用 children(复合组件)代替 renderX props 做组合。读完你可以掌握 render props 与 children 组合的取舍标准,并能结合仓库中同系列的复合组件、Context 接口规则,为组件库设计出可读、可组合、可依赖注入的 API。

规则定位:一条 MEDIUM 级组合模式

该文档是 Supabase 仓库 .claude/skills/vercel-composition-patterns/ 技能下的实现模式(patterns- 前缀)规则文件,其 Front Matter 声明如下:

title: Prefer Composing Children Over Render Props
impact: MEDIUM
impactDescription: cleaner composition, better readability
tags: composition, children, render-props

SKILL.md 的分级体系中,patterns- 类别属于第 3 优先级的"实现模式"(Impact: MEDIUM),与组件架构层(architecture-,HIGH)、状态管理(state-)并列。该技能的目标是"通过复合组件、状态提升与内部组合,避免 boolean prop 泛滥",并且"这些模式让人类和 AI Agent 在代码库扩展时都更容易维护"。这条 children 规则正是这套体系在调用方 API 形态上的具体落点。

核心规则:用 children 做组合

规则原文的结论是:

Use children for composition instead of renderX props. Children are more readable, compose naturally, and don't require understanding callback signatures.

(用 children 而非 renderX props 做组合。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>
  )
}

// Usage is awkward and inflexible
return (
  <Composer
    renderHeader={() => <CustomHeader />}
    renderFooter={() => (
      <>
        <Formatting />
        <Emojis />
      </>
    )}
    renderActions={() => <SubmitButton />}
  />
)

从源码结构看,这种 API 有三个痛点,恰好对应文档 Front Matter 中 "cleaner composition, better readability" 的动机:

  1. 调用方必须理解回调签名renderHeaderrenderFooterrenderActions 各自是 () => React.ReactNode,但哪个插槽渲染什么、内部顺序如何,调用方无法从 JSX 结构上直接看出,只能读组件实现;
  2. 使用方式笨拙(原文注释即 "Usage is awkward and inflexible"):多个箭头函数嵌套 JSX,缩进加深,diff 可读性差;
  3. 无法自然嵌套组合:插槽内容由函数注入而非 JSX 声明,子组件之间无法像普通 DOM 树那样直观地表达层级关系。

正例:children 驱动的复合组件

原文档给出的正确写法是把每个插槽变成独立的子组件,再用 children 声明式地编排:

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

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

// Usage is flexible
return (
  <Composer.Frame>
    <CustomHeader />
    <Composer.Input />
    <Composer.Footer>
      <Composer.Formatting />
      <Composer.Emojis />
      <SubmitButton />
    </Composer.Footer>
  </Composer.Frame>
)

对比反例的收益:

  • 结构即文档Composer.Frame > Header / Input / Footer 的嵌套关系直接表达了"头部在上、输入居中、底部操作区"的布局契约,调用方不需要查任何回调类型;
  • 组合自然<Composer.Footer> 内部放什么完全由调用方自由决定,新增/删减按钮不需要改动 Composer 的 props 类型;
  • 插槽默认值仍可实现Footer 接收 children,组件内部依然可以保留 children ? children : <DefaultFooter /> 之类的降级逻辑,只是组合入口从"函数参数"变成了"JSX 子节点"。

边界条件:什么时候 render props 仍然合适

原文档明确给出了 render props 的适用场景——当父组件需要向子节点回传数据时

// Render props work well when you need to pass data back
<List
  data={items}
  renderItem={({ item, index }) => <Item item={item} index={index} />}
/>

文档的总结论可以概括为一条判断准则:

Use render props when the parent needs to provide data or state to the child. Use children when composing static structure.

(父组件需要向子节点提供数据或状态时,用 render props;组合静态结构时,用 children。)

即:renderItem 的回调参数携带了只有 List 才知道的 itemindex,children 无法表达这种"数据下行",render props 是唯一自然形态;而 Composer 的三个插槽只是"占位结构",没有任何数据依赖,因此应改用 children。

纵深:该规则在同系列规则中的位置

这条 children 规则不是孤立的。结合仓库中同技能的其他规则文件,可以把它放进一个完整的设计链条中理解:

规则文件 Impact 解决的问题
architecture-avoid-boolean-props.md CRITICAL 不用 isThread/isEditing 等布尔 prop 做行为切换,改用组合
architecture-compound-components.md HIGH 用共享 Context 组织复合组件,子组件通过 context 而非 props 取状态
patterns-explicit-variants.md MEDIUM 用显式变体组件(ThreadComposerEditMessageComposer)替代布尔模式
patterns-children-over-render-props.md MEDIUM 静态结构组合用 children,数据下传用 render props
state-context-interface.md HIGH Context 接口拆分为 state / actions / meta,实现状态依赖注入
state-decouple-implementation.md MEDIUM Provider 是唯一知道状态如何管理的地方
react19-no-forwardref.md MEDIUM React 19 中 ref 作为普通 prop,use() 替代 useContext()

值得注意的一点是:patterns-children-over-render-props 的正例(Composer.Frame / Composer.Footer)与 architecture-compound-components.md 中的复合组件写法完全同构。该文件展示了对同一个 Composer 反例的完整演进——从带 showAttachmentsshowFormatting 等布尔 prop 的单体组件,演进为"共享 Context + 命名空间导出"的形态:

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

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

// Export as compound component
const Composer = {
  Provider: ComposerProvider,
  Frame: ComposerFrame,
  Input: ComposerInput,
  Submit: ComposerSubmit,
  Header: ComposerHeader,
  Footer: ComposerFooter,
  Attachments: ComposerAttachments,
  Formatting: ComposerFormatting,
  Emojis: ComposerEmojis,
}

也就是说,children 组合解决的是结构编排问题,而真正让 Composer.InputComposer.Submit 之间共享输入内容、提交动作的,是背后的 Context 接口(state / actions / meta 三段式,见 state-context-interface.md)。两者合起来才构成完整的复合组件方案:children 负责"摆什么",context 负责"怎么联动"。

适用前提:Supabase 仓库运行在 React 19

这套模式在 Supabase 仓库自身的前端代码中是成立的,因为仓库整体运行在 React 19 上:

  • pnpm-workspace.yaml 中 catalog 声明 react: ^19.2.6
  • pnpm-lock.yaml 中实际解析到 react@19.2.6
  • apps/studiopackages/ui 等应用的 react 依赖均以 catalog: 形式指向该版本。

因此 react19-no-forwardref.md 中的 React 19 API 变更同样适用于本仓库:ref 作为普通 prop 传入(不再需要 forwardRef),Context 读取优先使用 use() 而非 useContext()。例如同系列规则中所有子组件统一采用如下消费方式:

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

如果你在 React 18 或更早版本的项目中复用本文的模式,children / 复合组件 / Context 接口部分完全适用,但 use() 与 ref-as-prop 两项属于 React 19+ 专属 API,需要降级为 useContextforwardRef

决策清单

综合原文档与同系列规则,遇到"如何扩展一个组件的插槽"时可按以下顺序判断:

  1. 只是想让调用方摆放静态结构(头部、底部、操作区)→ 用 children + 复合组件(Composer.Frame / Composer.Footer 形态),这是本文档的核心结论;
  2. 父组件必须向子节点回传数据或状态(如列表项、上下文值)→ 用 render props(renderItem={({ item, index }) => ...} 形态);
  3. 组合的子组件之间还需要共享状态 → 补上共享 Context,接口按 state / actions / meta 拆分,由 Provider 依赖注入(参见 state-context-interface.md);
  4. 发现自己在用 showXisY 布尔 prop 控制插槽内容 → 停止加 prop,改为显式变体组件(参见 patterns-explicit-variants.md),例如用 ThreadComposerEditMessageComposer 各自组合所需的 Composer.* 子件,做到"每个变体显式声明自己用什么 provider、包含什么 UI、有什么操作",不存在布尔组合出的不可达状态。

一句话总结:children 让结构组合变得声明式且可读,render props 保留给"数据下行"这一类 children 表达不了的语义;两者再叠加共享 Context 的复合组件骨架,就是 Supabase 仓库这套 Vercel 组合模式技能推荐的组件 API 设计路径。

登录后查看全文
热门项目推荐
相关项目推荐