Supabase 仓库中的 React 架构规范:告别布尔 Prop 泛滥,用组合模式构建可维护组件
本篇基于 Supabase 仓库内置的 Vercel 组合模式规则 architecture-avoid-boolean-props.md 展开,讲解为什么布尔型 prop(isThread、isEditing 等)会让组件状态空间指数级膨胀,以及如何用复合组件(Compound Components)加共享 Context 的组合方案替代它。读完后你将掌握一套可直接用于组件重构与代码评审的判定标准:如何识别布尔 prop 泛滥、如何用 Composer.Frame / Composer.Input / Composer.Footer 这类组合 API 重写组件、并理解状态通过 state / actions / meta 三段式接口做依赖注入的底层机制。
一、问题本质:每个布尔 prop 都让状态空间翻倍
该规则在 vercel-composition-patterns 技能清单 中被归为第一优先级(Component Architecture / HIGH),元数据中标记为 impact: CRITICAL,其影响描述是 "prevents unmaintainable component variants"(防止不可维护的组件变体)。
文档给出的反面案例是一个消息发送框 Composer,用四个布尔 prop 控制行为:
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>
)
}
这段代码有三个层面的问题:
- 状态空间指数膨胀。每新增一个布尔 prop,组件的可能状态数就乘以 2。四个布尔 prop 意味着 16 种组合,而其中不少组合在业务上是非法的(例如
isEditing与isForwarding同时为 true),但类型系统无法阻止调用方传出不可能的组合。 - 条件逻辑集中且互相纠缠。
isDMThread与isThread共用一段三元表达式,isEditing/isForwarding又共用另一段——阅读者必须沿着所有分支才能回答"这个组件在某个调用点到底渲染了什么"。 - 数据 prop 与行为 prop 混杂。
channelId、dmId是纯数据,却被布尔 prop 决定"是否生效",prop 之间存在隐式的依赖约束(isThread为 true 时channelId才必须有值),而这类约束在 props 签名中完全不可见。
二、解法:用组合替代条件分支
文档给出的正确写法是把一个单体组件拆成三个显式变体,每个变体声明式地组合它需要的内部件:
// Channel composer
function ChannelComposer() {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Attachments />
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
// Thread composer - adds "also send to channel" field
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>
)
}
// Edit composer - different footer actions
function EditComposer() {
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.CancelEdit />
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
)
}
对比原文的三元条件链,这个方案的关键转变是:变体之间共享的是内部件(Composer.Input、Composer.Footer 等),而不是共享一个带大量开关的父组件。文档的结论性一句话是:"Each variant is explicit about what it renders. We can share internals without sharing a single monolithic parent."(每个变体明确知道自己渲染什么;我们可以共享内部件,而不必共享一个单体的父组件。)
这与同技能包中的 patterns-explicit-variants.md 规则是一体两面:调用方从
<Composer
isThread
isEditing={false}
channelId='abc'
showAttachments
showFormatting={false}
/>
变为
<ThreadComposer channelId="abc" />
// 或 <EditMessageComposer messageId="xyz" />
// 或 <ForwardMessageComposer messageId="123" />
前者需要调用方理解六个 prop 的相互约束才能推断渲染结果;后者"代码即文档"——组件名直接说明渲染的是什么,且不存在不可能的状态组合。
三、底层机制:复合组件与共享 Context
变体只是表象,真正让"共享内部件"成立的是复合组件结构。同技能包的 architecture-compound-components.md 给出了内部实现:Composer 不是一个函数,而是一个把子组件挂载为属性的对象,子组件之间通过 Context 而非 props 传递状态:
const ComposerContext = createContext<ComposerContextValue | null>(null)
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
return (
<ComposerContext value={{ state, actions, meta }}>
{children}
</ComposerContext>
)
}
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>
}
// Export as compound component
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>
这里有两点值得注意:
- Context 承载状态,children 承载结构。
Frame只负责把children包进<form>,Input/Submit各自从 Context 取数据——结构组合(谁包含谁)与数据流(谁读写状态)被彻底分开。这也呼应了 patterns-children-over-render-props.md 的结论:静态结构用children组合,只有当父组件需要把数据回传给子组件时才使用 render props(例如<List data={items} renderItem={({ item, index }) => ...} />)。 - 代码中使用
use(ComposerContext)而非useContext。这与技能清单中react19-no-forwardref规则一致:React 19 起可用use()读取 Context,且ref可以作为普通 prop 传递,无需forwardRef。需要注意该条规则在 SKILL.md 中明确标注 "React 19+ only"——如果项目仍停留在 React 18 或更早版本,应改回useContext写法。
四、状态如何被解耦:state / actions / meta 三段式接口
组合模式要避免下一个陷阱:内部件直接调用某个具体 hook(如 useChannelComposerState()),导致 UI 与该状态实现耦合。state-context-interface.md 给出的是通用接口定义——Context 的值由 state、actions、meta 三部分构成:
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
}
这样任何 Provider 都可以实现同一接口:一个用 useState 存本地草稿(转发消息场景),另一个用全局同步 hook(频道场景,对应 patterns-explicit-variants.md 中的 ChannelProvider / EditMessageProvider / ForwardMessageProvider 各包一层 Provider 再组合 Composer.Frame 的写法)。UI 组件只消费接口,替换 Provider 即可替换状态实现,而 Composer.Frame / Composer.Input 等 UI 原封不动。Provider 边界同样允许"结构在框外、状态在框内"的用法:按钮与预览组件即使不在 Composer.Frame 内部,只要位于 Provider 内就能调用 submit 或读取 state.input——决定能力的是 Provider 边界,不是视觉嵌套。
五、对照仓库实际代码:布尔"样式变体"与布尔"行为开关"的界限
规则针对的是用布尔 prop 控制组件行为分支的情况。Supabase 仓库自身的 UI 库中有一个值得对照的例子:设计系统组件库的 TreeView 组件(packages/ui/src/components/TreeView/TreeView.tsx)也使用了 isSelected、isOpened、isPreview 这类布尔型 prop,但它走的是 class-variance-authority(cva)路线:
const TreeViewItemVariant = cva('group relative transition-colors h-[28px] ...', {
variants: {
isSelected: {
true: 'text-foreground !bg-selection',
false: '',
},
isOpened: {
true: 'bg-control',
false: '',
},
isPreview: {
true: 'bg-control text-foreground',
false: '',
},
},
})
从源码结构看,这里的布尔 prop 只是声明式的样式类映射:每个取值对应固定的 Tailwind 类名,不存在 if/else 渲染逻辑、不存在 prop 之间的组合约束,状态空间由 cva 的类型系统显式枚举。这与规则要治理的"isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : ..."式行为分支有本质区别——后者是运行时条件逻辑的堆叠,前者是编译期穷举的样式变体。可以推断,该规则的适用边界是:布尔 prop 一旦驱动了不同的 DOM 结构、不同的数据流或不同的行为,就应该改用组合或显式变体;若只是穷举样式状态,cva 式的变体 prop 仍是合理选择。
另值得注意的是该文件仍在使用 forwardRef(TreeView.tsx 第 47 行)。这与本技能包中 react19- 规则的方向不同——属于存量代码尚未迁移到 React 19 API 的状态,阅读时以当前仓库实际写法为准即可。
六、适用时机与落地检查清单
按 SKILL.md 的"When to Apply",该规则集适用于以下场景:重构拥有大量布尔 prop 的组件、构建可复用组件库、设计灵活的组件 API、评审组件架构,以及涉及复合组件或 Context Provider 的工作。规则按四个优先级组织:architecture-(HIGH)> state-(MEDIUM)> patterns-(MEDIUM)> react19-(MEDIUM,仅 React 19+),本文涉及的 architecture-avoid-boolean-props 位列其中优先级最高的一条。
在实际评审或重构时,可以按以下顺序自检:
- 数一数:组件 props 中有多少布尔值?≥ 3 个且开始出现在三元表达式或
&&链中,即为信号; - 列组合:列出布尔组合,标记业务上非法的组合——非法组合越多,越需要拆分为显式变体组件;
- 拆内部件:把渲染片段抽成
Xxx.Frame / Xxx.Input / Xxx.Footer等无副作用的共享内部件,内部件间状态走 Context; - 建变体:为每种合法用法建一个命名组件(
ThreadComposer、EditComposer),让"渲染什么"由组件名表达; - 定义接口:Context 值按
state / actions / meta三段定义,Provider 注入状态实现,使 UI 与具体 hook 解耦、可被多个 Provider 复用。
这套模式的价值最终落在两点:对人类读者,变体代码自文档化,无需在脑中运行条件分支;对 AI 辅助工具,每个组件的渲染结果可从其源码静态确定,显著降低了大规模重构时的推理与出错成本。
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 StartedRust0625
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