React 学习站
React›核心›核心

组合模式与 Children

核心核心

一句话定义

把 JSX 通过 children 或任意 props 传进组件(children 就是 JSX 类型的 prop),让父组件决定内容、子组件决定框架——这是 React 官方推荐的替代「十几个配置 props」的设计方式。

为什么重要

组件设计的分水岭:用 open size closable 这类「开关式 props」堆出来的组件,每加一个需求就要改组件源码;用 children 组合的组件,把「内容是什么」的决策权交给使用者,组件只需管好布局与交互——这是从「写组件」到「设计组件库」的跨越。

前置知识

  • kp-004 props 机制。
  • kp-002 JSX 是表达式(因此可以当值传递)。

核心概念

  • 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 的交叉点)。

自测题

  1. 用 children 组合重构一个 size/title/showClose/okText/cancelText 五参数的 Dialog,列出设计收益。
  2. children 的类型可能是哪三种?函数式 children 解决什么问题?
  3. 为什么说「Provider + children」是组合模式?slot 模式与 children 的取舍标准是什么(一个插槽 vs 多个插槽)?
  4. 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>