一句话定义
把 JSX 通过 children 或任意 props 传进组件(children 就是 JSX 类型的 prop),让父组件决定内容、子组件决定框架——这是 React 官方推荐的替代「十几个配置 props」的设计方式。
为什么重要
组件设计的分水岭:用 open size closable 这类「开关式 props」堆出来的组件,每加一个需求就要改组件源码;用 children 组合的组件,把「内容是什么」的决策权交给使用者,组件只需管好布局与交互——这是从「写组件」到「设计组件库」的跨越。
前置知识
核心概念
- children:标签之间的内容,作为特殊 prop 传入;类型可以是元素、数组、函数。
- Slot 模式:用普通 props 传 JSX(
header={<Title/>} footer={<Actions/>})实现多插槽。 - children 作为函数(render props / FACC):把「渲染逻辑」当回调,适合「我拿数据、你画样子」。
- 组合优于配置:能用 children/slot 定制的,不要加布尔开关。
- Context 注入:Provider 本身就是组合模式的最大案例——children 里的任意层可消费。
原理 / 机制
从「配置地狱」到「组合」的重构对比:
jsx// ❌ 配置式:每加一种布局都要加 prop,且分支爆炸
<Modal open size="lg" closable showHeader showFooter
headerText="确认" footerAlign="right" ...>
// ✅ 组合式:Modal 只管骨架与交互,内容全部由使用者拼装
function Modal({ open, onClose, children }) {
if (!open) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}
<Modal open={open} onClose={close}>
<Modal.Header>确认删除</Modal.Header>
<Modal.Body>该操作不可撤销,确定继续?</Modal.Body>
<Modal.Footer align="right">
<Button variant="ghost" onClick={close}>取消</Button>
<Button variant="danger" onClick={doDelete}>删除</Button>
</Modal.Footer>
</Modal>
Modal.Header 这类子组件挂载(namespace components)是组件库的标准语法。
children 作为函数(数据管道):
jsxfunction Fetch({ url, children }) {
const [state, setState] = useState({ status: 'loading' });
useEffect(() => {
let ignore = false;
fetch(url).then(r => r.json())
.then(data => { if (!ignore) setState({ status: 'ok', data }); });
return () => { ignore = true; };
}, [url]);
return children(state); // 渲染逻辑由使用者决定
}
<Fetch url="/api/user">
{state => state.status === 'loading' ? <Spinner/> : <Profile user={state.data}/>}
</Fetch>
注意:现代实践中这类「数据 + 渲染回调」更多交给 Suspense(kp-020)或数据层(kp-021);render props 保留在「通用逻辑插槽」场景。
直观类比
配置式组件像点套餐:套餐里有什么你说了不算,想吃辣只有「加辣开关」,想换主食没有选项。组合式像自助餐台:餐厅(Modal)提供桌椅、餐盘与秩序(布局、遮罩、动画),菜怎么配(children)完全由你。children 是「你来掌勺」,props 开关是「帮我少放点葱」。
实例 / 案例
多插槽卡片(slot props):
jsxfunction Card({ header, footer, children }) {
return (
<section className="card">
{header && <header className="card-h">{header}</header>}
<div className="card-b">{children}</div>
{footer && <footer className="card-f">{footer}</footer>}
</section>
);
}
<Card
header={<h3>每周精选</h3>}
footer={<a href="/more">查看全部 →</a>}
>
<ArticleList />
</Card>
布局组件天然适合 children——<Layout><Sidebar/><Main/></Layout> 中 Layout 对内容一无所知,却可以统一处理内边距、滚动与响应式。
常见误区
- children 拿来就用不校验场景:
<Modal><input/></Modal>能渲染,但把「应该放 Header/Footer」的约定丢了;namespace 子组件的作用之一就是引导正确用法。 - 在 children 上做深度变换依赖
React.Children:该 API 在并发/RSC 时代限制增多(只适用于展示微调);需要结构化传内容时用显式 slot props。 - 把布尔开关堆成「伪组合」:
<Card bordered shadow padded compact hoverable>—— 与其开关,不如让使用者传 className 或换变体组件。 - 忘记 children 的 re-render 联动:children 是父组件创建的元素;父渲染 → children 元素新引用 → 子组件(若被 memo)击穿(kp-018/019 的交叉点)。
自测题
- 用 children 组合重构一个
size/title/showClose/okText/cancelText五参数的 Dialog,列出设计收益。 children的类型可能是哪三种?函数式 children 解决什么问题?- 为什么说「Provider + children」是组合模式?slot 模式与 children 的取舍标准是什么(一个插槽 vs 多个插槽)?
- memo 的 List 组件接收
<List>{items.map(...)}</List>为什么总是重渲染?
与其他知识点的关系
- 向前:kp-003「拆组件」的进阶版——不只是拆,还设计接口。
- 向后:kp-020 的
<Suspense fallback={...}>{children}</Suspense>、kp-024 的 Server Component 向 Client Component 传 children(序列化边界),都是组合模式的平台级应用;kp-028 把「布尔 prop 堆积」列入审查项。
延伸阅读
- Passing JSX as children:<https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children>
- Composition vs Inheritance(旧文档仍值得读):<https://legacy.reactjs.org/docs/composition-vs-inheritance.html>