- 前端
- 教程
【免费下载链接】preguntas-entrevista-react
Preguntas típicas sobre React para entrevistas de trabajo ⚛️
复合组件(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 = 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> } // 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} channelId='abc' />时,根本无法从 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 全部收进专用 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 /> {/* 自定义组件可访问状态和动作 */} <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.RefObject<TextInput> } interface ComposerContextValue { state: ComposerState actions: ComposerActions meta: ComposerMeta } const ComposerContext = createContext<ComposerContextValue | 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 channelId="abc"> <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()),一旦状态实现换成服务端同步或 Zustand,UI 就得跟着改。用规则的总结来说:UI 是可复用积木,状态由 Provider 依赖注入——换掉 Provider,UI 原样保留。
更进一步,由于 Provider 边界(而非视觉嵌套)才是能力范围,你可以在Composer.Frame之外、Provider 之内安放自定义 UI,例如对话框底部的前往按钮与消息预览组件,它们照样能读取 Composer 的状态与动作。这正是"组合 + 依赖注入"组合拳的威力所在。
纵深支撑三:React 19 API 变化对复合组件的影响
当前仓库(preguntas-entrevista-react)的 package.json 中声明react/react-dom为19.2.8,@types/react为 19.2.18,因此本技能中 React 19 相关的约定在仓库环境下是适用的。技能 react19-no-forwardref.md 明确了两条影响复合组件书写的 API 变化(⚠️ 仅适用于 React 19,React 18 及更早版本请跳过):
变化一:ref变成普通 prop,不再需要forwardRef。
反模式(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} /> }变化二:用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 对复合组件模式给出了独立佐证:它将该模式定义为"创建一个单一目标的父组件,向其子组件提供渲染所需的属性",并指出其价值在于声明式结构、可读性与简洁性,还给出了List+ListItem的基础示例:
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,配合复合组件共享 Context | state-lift-state / architecture-compound-components |
| 同一套 UI 要驱动多种状态来源(本地/全局/服务端) | 定义 state/actions/meta 通用 Context 接口,换 Provider 不换 UI | state-context-interface |
| 父组件需要向子组件回传数据(如列表 item 与 index) | 此时才用 render prop | patterns-children-over-render-props |
| 项目处于 React 19 | 去掉 forwardRef,用use()读 Context | react19-no-forwardref |
复合组件不是万能的:如果部件之间本就不共享状态,直接children组合即可,不必引入 Context;如果需要回传数据的场景(如虚拟列表),render prop 依然是正确选择。技能的判断标准始终一致——让 JSX 直接表达"渲染什么",把条件逻辑与状态实现从 UI 中剥离出去。
<输出文章>
- 前端
- 教程
【免费下载链接】preguntas-entrevista-react
Preguntas típicas sobre React para entrevistas de trabajo ⚛️
相关推荐
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 drilling
Langfuse React 组件架构实践:用 Compound Components 与共享 Context 替代 prop drilling 本篇技术指南以
人工智能LLMOps可观测性AI 评测LLM 网关后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考