拓冰建站拓冰建站
首页 / 资讯中心 / 正文

React 复合组件(Compound Components)架构实战:用共享 Context 构建可组合组件——vercel-composition-patterns 规则深度解析

前端教程【免费下载链接】preguntas-entrevista-reactPreguntas típicas sobre React para entrevistas de trabajo ⚛️项目地址https://gitcode.com/gh_mirrors/pr/preguntas-entrevista-react点击查看免费下载复合组件Compound Components是 React 组件架构中解决配置式膨胀与prop drilling的核心模式用共享 Context 取代逐层透传的 props让子组件按需读取状态与动作消费者自由拼装 UI 片段。本文以当前仓库 vercel-composition-patterns 技能中的 architecture-compound-components.md 规则为主体结合其关联的布尔 prop、状态提升、Context 接口、React 19 API 等规则讲解如何将一个单一巨石组件 render props 一堆开关改造成复合组件 共享 Context 显式组合读完你将掌握一套可复制、可运行、可规模化的组件重构方法论。规则定位为什么复合组件是 HIGH 影响级的架构决策在该技能体系中architecture-compound-components.md属于Component Architecture组件架构类别frontmatter 中标注的影响等级为HIGH其影响描述是enables flexible composition without prop drilling —— 实现灵活组合同时消除 prop drilling。与它同属架构类、影响等级达到 CRITICAL 的是 architecture-avoid-boolean-props.md避免布尔 prop 爆炸。两条规则互为表里布尔 prop 是病复合组件是药。技能总纲在 SKILL.md 中给出了该技能的触发场景重构大量布尔 prop 的组件、构建可复用组件库、设计灵活的组件 API、审查组件架构、处理复合组件与 Context Provider。在深入代码之前先看该技能的核心原则来自 README.md组合优于配置Composition over configuration——不要靠加 prop让消费者自己拼装提升你的状态Lift your state——状态放在 Provider 中而不是困在组件内部组合你的内部实现Compose your internals——子组件通过 Context 取数据而不是通过 props显式变体Explicit variants——创建ThreadComposer、EditComposer而不是靠isThread切换的Composer。复合组件正是第 1、3 条原则的直接落地。反模式巨石组件 render props 布尔开关规则文档给出的第一个反面例子是一个典型的配置式巨石组件用renderHeader、renderFooter、renderActions三个 render prop 注入 UI用showAttachments、showFormatting、showEmojis三个布尔开关控制内部渲染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 ) }这个写法的问题可以归纳为三点隐藏的条件逻辑组件内部埋着大量showX X /三元与短路判断调用方根本无法从 JSX 层看出这个 Composer 到底渲染了什么只能靠读 props 清单猜测回调签名的心智负担render prop 要求调用方理解每个回调的签名与调用时机嵌套多个 render prop 时 JSX 结构被强行拆散可读性差不可组合的全有或全无footer 的默认分支与自定义分支互斥想要默认 footer 加一个自定义动作时被迫复制整段默认结构。关于 render prop 与 children 的取舍技能中有专门的规则 patterns-children-over-render-props.md当父组件需要向子组件回传数据/状态时才用 render prop典型如List data{items} renderItem{({item, index}) ...} /当组合的是静态结构时一律用 children。正模式复合组件 共享 Context规则给出的正确做法是把 Composer 拆成一组通过同一个 Context 协作的复合子组件并以命名空间对象导出const ComposerContext createContextComposerContextValue | 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 } // Export as compound component const Composer { Provider: ComposerProvider, Frame: ComposerFrame, Input: ComposerInput, Submit: ComposerSubmit, Header: ComposerHeader, Footer: ComposerFooter, Attachments: ComposerAttachments, Formatting: ComposerFormatting, Emojis: ComposerEmojis, }这里有四个值得注意的要点子组件通过 Context 而非 props 取数据ComposerInput、ComposerSubmit各自use(ComposerContext)解构出state、actions、meta不需要父级逐层传参prop drilling 消失Context 值采用state / actions / meta三段式结构这正是技能中 state-context-interface.md 定义的通用接口契约——state是纯数据、actions是修改状态的动作、meta是 ref 等非渲染型附属物。任何 Provider 只要实现这一接口同一套 UI 子组件就能直接复用子组件之间完全解耦ComposerFrame只管form容器ComposerInput只管输入框彼此不知道对方存在也不存在任何父级隐藏条件命名空间导出const Composer { Provider, Frame, Input, ... }把一组子组件收拢成单一导入入口调用方import { Composer }即可使用所有部件。调用方视角显式组合所需的一切复合组件最有价值的体现是调用侧——消费者不再配置一个巨石组件而是拼装自己需要的部件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正如规则原文所总结的Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.消费者显式地组合他们需要的部分没有任何隐藏条件state、actions、meta 由父级 Provider 依赖注入因此同一套组件结构可以被多处复用以承载不同用途。想加附件区域在Composer.Frame里插入Composer.Attachments /即可不想要 Header直接不写那一行。JSX 本身就是文档组件渲染什么一目了然。配套规则避免布尔 prop 爆炸CRITICAL复合组件要解决的病根在 architecture-avoid-boolean-props.md 中讲得很透彻每增加一个布尔 prop组件可能的状态数量就翻倍。一个带isThread、isEditing、isDMThread的组件其内部条件组合呈指数级增长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 ) }试想调用方写Composer isThread isEditing{false} channelIdabc /时根本无法从 JSX 判断实际渲染结果。而改用复合组件后每种场景是一个显式变体组件各自组合自己需要的部件、使用自己需要的 Provider详见技能 patterns-explicit-variants.md// 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 - 额外加一个 also send to channel 字段 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 - 不同的 footer 动作 function EditComposer() { return ( Composer.Frame Composer.Input / Composer.Footer Composer.Formatting / Composer.Emojis / Composer.CancelEdit / Composer.SaveEdit / /Composer.Footer /Composer.Frame ) }每个变体组件都显式声明了使用哪个 Provider即哪种状态来源、包含哪些 UI 部件、暴露哪些动作。没有布尔组合需要推理也没有不可能存在的状态组合。变体之间依然可以共享Composer.*内部部件但不再共享一个巨石父组件。纵深支撑一状态提升到 Provider让视觉之外的组件也能访问状态复合组件的另一半功力来自状态提升。技能中 state-lift-state.md 指出需要共享状态的组件不必在视觉上嵌套在一起只要处于同一个 Provider 之内即可。先看反例——状态被困在组件内部对话框里的预览与提交按钮就无法访问它function ForwardMessageComposer() { const [state, setState] useState(initialState) const forwardMessage useForwardMessage() return ( Composer.Frame Composer.Input / Composer.Footer / /Composer.Frame ) } // Problem: 这个按钮怎么访问 composer 的状态 function ForwardMessageDialog() { return ( Dialog ForwardMessageComposer / MessagePreview / {/* 需要 composer 状态 */} DialogActions CancelButton / ForwardButton / {/* 需要调用 submit */} /DialogActions /Dialog ) }该规则还列举了另外两种错误补救方案用useEffect把内部状态同步到父级每次输入变化都触发一次同步性能与心智成本都高以及提交时从ref里读状态绕过渲染模型拿不到最新值且无法触发更新。正确做法是把状态、动作、meta 全部收进专用 Providerfunction 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 / {/* 自定义组件可访问状态和动作 */} DialogActions CancelButton / ForwardButton / {/* 自定义组件可访问状态和动作 */} /DialogActions /Dialog /ForwardMessageProvider ) } function ForwardButton() { const { actions } use(Composer.Context) return Button onPress{actions.submit}Forward/Button }关键洞察是ForwardButton和MessagePreview在视觉上并不位于Composer.Frame内部但只要它们处于ForwardMessageProvider的作用范围内就能通过use(Composer.Context)读写 Composer 的状态与动作——这就是把状态提升进 Provider带来的能力边界扩展。纵深支撑二state / actions / meta 通用接口与依赖注入复合组件可复用性的根基是 state-context-interface.md 定义的三段式通用 Context 接口// 定义一个任何 Provider 都能实现的通用接口 interface ComposerState { input: string attachments: Attachment[] isSubmitting: boolean } interface ComposerActions { update: (updater: (state: ComposerState) ComposerState) void submit: () void } interface ComposerMeta { inputRef: React.RefObjectTextInput } interface ComposerContextValue { state: ComposerState actions: ComposerActions meta: ComposerMeta } const ComposerContext createContextComposerContextValue | null(null)这套接口是契约而非实现任何 Provider 只要实现了state / actions / meta三个字段就能驱动同一套Composer.*UI 部件。于是换 Provider 不换 UI成为可能——本地临时表单用useState频道场景用全局同步状态两种 Provider 接入的是完全相同的界面代码// 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 ) }同样的组合 UI 可以无缝工作在两种 Provider 之下// 与 ForwardMessageProvider本地状态一起工作 ForwardMessageProvider Composer.Frame Composer.Input / Composer.Submit / /Composer.Frame /ForwardMessageProvider // 与 ChannelProvider全局同步状态一起工作 ChannelProvider channelIdabc Composer.Frame Composer.Input / Composer.Submit / /Composer.Frame /ChannelProvider对应地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 }))} / ) }反模式则是让 UI 直接调用具体业务 hook如useChannelComposerState()一旦状态实现换成服务端同步或 ZustandUI 就得跟着改。用规则的总结来说UI 是可复用积木状态由 Provider 依赖注入——换掉 ProviderUI 原样保留。更进一步由于 Provider 边界而非视觉嵌套才是能力范围你可以在Composer.Frame之外、Provider 之内安放自定义 UI例如对话框底部的前往按钮与消息预览组件它们照样能读取 Composer 的状态与动作。这正是组合 依赖注入组合拳的威力所在。纵深支撑三React 19 API 变化对复合组件的影响当前仓库preguntas-entrevista-react的 package.json 中声明react/react-dom为19.2.8types/react为 19.2.18因此本技能中 React 19 相关的约定在仓库环境下是适用的。技能 react19-no-forwardref.md 明确了两条影响复合组件书写的 API 变化⚠️ 仅适用于 React 19React 18 及更早版本请跳过变化一ref变成普通 prop不再需要forwardRef。反模式React 19 下是继续用forwardRef包装const ComposerInput forwardRefTextInput, Props((props, ref) { return TextInput ref{ref} {...props} / })正确写法是把ref当普通 prop 接收function ComposerInput({ ref, ...props }: Props { ref?: React.RefTextInput }) { return TextInput ref{ref} {...props} / }变化二用use()取代useContext()。反模式const value useContext(MyContext)正确写法const value use(MyContext)use()的额外能力是可以条件调用这是useContext()做不到的后者受 hooks 规则约束必须在组件顶层无条件调用。这一点让复合组件的子组件在读取 Context 时获得更大灵活性。注意规则文档中的复合组件示例如ComposerInput中的use(ComposerContext)正是基于这一新 API 的写法。在本仓库中的落地参考本仓库本身是一个 React 面试题项目preguntas-entrevista-react其中 que-es-el-compound-components-pattern.json 对复合组件模式给出了独立佐证它将该模式定义为创建一个单一目标的父组件向其子组件提供渲染所需的属性并指出其价值在于声明式结构、可读性与简洁性还给出了ListListItem的基础示例const List ({ children, ...props }) ul {...props}{children}/ul const ListItem ({ children, ...props }) { return li {...props}{children}/li } export { List, ListItem }这个最小示例与规则中的Composer复合组件一脉相承父部件List/Composer.Frame只负责承载 children具体内容由调用方组合。可以推断仓库中的这些内容条目与.agents下的技能规则形成了理论定义 工程规范的互补关系——前者回答复合组件是什么后者回答复杂组件如何按复合组件模式重构。实战速查何时用复合组件何时换其他方案综合技能总纲 SKILL.md 与各规则文件给出以下决策清单场景推荐做法依据规则组件开始堆砌isXxx布尔 prop拆成复合组件 显式变体组件architecture-avoid-boolean-props / patterns-explicit-variants需要让对话框、按钮等视觉外组件访问表单状态把状态提升进 Provider配合复合组件共享 Contextstate-lift-state / architecture-compound-components同一套 UI 要驱动多种状态来源本地/全局/服务端定义 state/actions/meta 通用 Context 接口换 Provider 不换 UIstate-context-interface父组件需要向子组件回传数据如列表 item 与 index此时才用 render proppatterns-children-over-render-props项目处于 React 19去掉 forwardRef用use()读 Contextreact19-no-forwardref复合组件不是万能的如果部件之间本就不共享状态直接children组合即可不必引入 Context如果需要回传数据的场景如虚拟列表render prop 依然是正确选择。技能的判断标准始终一致——让 JSX 直接表达渲染什么把条件逻辑与状态实现从 UI 中剥离出去。输出文章赞分享前端教程【免费下载链接】preguntas-entrevista-reactPreguntas típicas sobre React para entrevistas de trabajo ⚛️项目地址https://gitcode.com/gh_mirrors/pr/preguntas-entrevista-react点击查看免费下载相关推荐OpenMontage 复合组件架构实战用共享 Context 构建可组合、可依赖注入的 React 组件体系OpenMontage 复合组件架构实战用共享 Context 构建可组合、可依赖注入的 React 组件体系 导读 本文是 OpenMontage 仓库中人工智能AI Agent音视频媒体生成工作流自动化Comp AI CRM 中实践复合组件Compound Components模式以共享 Context 取代 render props 构建可扩展的 React 组件架构Comp AI CRM 中实践复合组件Compound Components模式以共享 Context 取代 render props 构建可扩展的 Re后端前端CRM人工智能AI AgentLangfuse React 组件架构实践用 Compound Components 与共享 Context 替代 prop drillingLangfuse React 组件架构实践用 Compound Components 与共享 Context 替代 prop drilling 本篇技术指南以人工智能LLMOps可观测性AI 评测LLM 网关后端前端上一篇Diem Move IR 编译器compiler源码级指南从 .mvir 到 Move 字节码的编译、验证与实战下一篇Volcano Job 优先级配置指南Job 级 PriorityClassName 默认值与 Task 级覆盖机制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门