Supabase 仓库实战:React 组件组合模式——为什么 children 组合优于 render props
本文基于 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
childrenfor composition instead ofrenderXprops. Children are more readable, compose naturally, and don't require understanding callback signatures.(用
children而非renderXprops 做组合。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" 的动机:
- 调用方必须理解回调签名:
renderHeader、renderFooter、renderActions各自是() => React.ReactNode,但哪个插槽渲染什么、内部顺序如何,调用方无法从 JSX 结构上直接看出,只能读组件实现; - 使用方式笨拙(原文注释即 "Usage is awkward and inflexible"):多个箭头函数嵌套 JSX,缩进加深,diff 可读性差;
- 无法自然嵌套组合:插槽内容由函数注入而非 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 才知道的 item 与 index,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 | 用显式变体组件(ThreadComposer、EditMessageComposer)替代布尔模式 |
| 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 反例的完整演进——从带 showAttachments、showFormatting 等布尔 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.Input 与 Composer.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/studio与packages/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,需要降级为 useContext 与 forwardRef。
决策清单
综合原文档与同系列规则,遇到"如何扩展一个组件的插槽"时可按以下顺序判断:
- 只是想让调用方摆放静态结构(头部、底部、操作区)→ 用 children + 复合组件(
Composer.Frame/Composer.Footer形态),这是本文档的核心结论; - 父组件必须向子节点回传数据或状态(如列表项、上下文值)→ 用 render props(
renderItem={({ item, index }) => ...}形态); - 组合的子组件之间还需要共享状态 → 补上共享 Context,接口按
state/actions/meta拆分,由 Provider 依赖注入(参见 state-context-interface.md); - 发现自己在用
showX、isY布尔 prop 控制插槽内容 → 停止加 prop,改为显式变体组件(参见 patterns-explicit-variants.md),例如用ThreadComposer、EditMessageComposer各自组合所需的Composer.*子件,做到"每个变体显式声明自己用什么 provider、包含什么 UI、有什么操作",不存在布尔组合出的不可达状态。
一句话总结:children 让结构组合变得声明式且可读,render props 保留给"数据下行"这一类 children 表达不了的语义;两者再叠加共享 Context 的复合组件骨架,就是 Supabase 仓库这套 Vercel 组合模式技能推荐的组件 API 设计路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00