解析 React Composition Patterns

在 React 的世界里,有一个核心的设计哲学:组合优于继承(Composition over Inheritance)。React 官方极少推荐使用继承来构建组件,而是通过强大的组合模式(Composition Patterns)来实现代码的复用、解耦和灵活性。

下面为你详细拆解 React 中最常见且实用的几种组合模式,从基础到高级,帮助你在实际开发中游刃有余。


1. 基础组合:props.children(容器模式)

这是最直观、最常用的组合方式。组件作为一个"壳子"(Container),它不知道也不关心内部会渲染什么具体内容,而是将控制权交给父组件。

示例场景:弹窗(Dialog)或卡片(Card)

jsx 复制代码
// 子组件:作为一个通用的边框容器
function Card(props) {
  return (
    <div
      className="card-wrapper"
      style={{ border: '1px solid #ccc', padding: '20px' }}
    >
      {props.children} {/* 这里会渲染父组件传入的任何内容 */}
    </div>
  )
}

// 父组件:决定容器内部的具体内容
function App() {
  return (
    <Card>
      <h2>文章标题</h2>
      <p>这是卡片的内容主体...</p>
      <button>点赞</button>
    </Card>
  )
}

2. 插槽模式(Component Injection / Slots)

有时候,仅仅通过 props.children 顺次排列渲染是不够的。如果你的组件内部有多个特定的"预留位置"(比如网页的头部、侧边栏、主体内容),你可以直接将 React 元素(Components)作为普通的 props 传进去。这类似于 Vue 中的"具名插槽"。

示例场景:通用布局(Layout)

jsx 复制代码
// 布局组件
function AppLayout({ header, sidebar, content }) {
  return (
    <div className="layout-container">
      <header className="site-header">{header}</header>
      <div className="layout-body">
        <aside className="site-sidebar">{sidebar}</aside>
        <main className="site-main">{content}</main>
      </div>
    </div>
  )
}

// 使用布局
function App() {
  return (
    <AppLayout
      header={<NavigationMenu />}
      sidebar={<UserSidebar />}
      content={<DashboardMain />}
    />
  )
}

优势:打破了只能按上下顺序嵌套的限制,可以把组件精准放置在页面的任何结构中。


3. 复合组件模式(Compound Components)

这是编写高级 UI 组件库(如 Radix UI, Ant Design)最常用的黑魔法。它允许你创建一组相互关联的组件,这些组件共享隐式的状态,从而让使用者以极具可读性、声明式的方式编写 HTML。

示例场景:下拉菜单(Select)或标签页(Tabs)

如果没有复合组件,你可能需要写一个极其复杂的配置型组件:<Select options={[{...}, {...}]} onChange={...} />。

而使用复合组件模式,你可以这样写:

jsx 复制代码
import React, { useState, createContext, useContext } from 'react'

// 1. 创建一个上下文用于共享状态
const ToggleContext = createContext()

// 2. 主组件(外层包裹)
function Toggle({ children }) {
  const [on, setOn] = useState(false)
  const toggle = () => setOn(!on)

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  )
}

// 3. 子组件:开关按钮
Toggle.Button = function ToggleButton() {
  const { toggle } = useContext(ToggleContext)
  return <button onClick={toggle}>切换状态</button>
}

// 4. 子组件:开启时显示的内容
Toggle.On = function ToggleOn({ children }) {
  const { on } = useContext(ToggleContext)
  return on ? <div>{children}</div> : null
}

// 5. 子组件:关闭时显示的内容
Toggle.Off = function ToggleOff({ children }) {
  const { on } = useContext(ToggleContext)
  return !on ? <div>{children}</div> : null
}

// 使用它:极其直观和灵活!
function App() {
  return (
    <Toggle>
      <Toggle.Button />
      <Toggle.On>💡 灯亮了</Toggle.On>
      <Toggle.Off>❌ 灯滅了</Toggle.Off>
    </Toggle>
  )
}

优势 :调用者不需要手动在每个子组件之间传递 on 或 toggle 状态,组件结构清晰,扩展性极强。


4. 渲染属性模式(Render Props)

Render Props 模式是指:组件不自己决定渲染什么,而是接受一个返回 React 元素的函数,并将内部的状态作为参数传递给这个函数。

虽然现代 React 开发中很多 Render Props 已被 Custom Hooks 取代,但在某些需要动态控制 UI 渲染结构的场景中,它依然非常强大。

示例场景:水流/滚动监听、鼠标位置追踪

jsx 复制代码
import React, { useState } from 'react'

// 负责逻辑和状态的组件
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  const handleMouseMove = event => {
    setPosition({ x: event.clientX, y: event.clientY })
  }

  return (
    <div
      style={{ height: '200px', border: '1px solid' }}
      onMouseMove={handleMouseMove}
    >
      {/* 关键:把状态传给 render 函数 */}
      {render(position)}
    </div>
  )
}

// 使用它
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <h1>
          当前鼠标位置:X: {x}, Y: {y}
        </h1>
      )}
    />
  )
}

5. 现代逻辑组合:Custom Hooks

在 React 16.8 之后,Hooks 成了纯逻辑组合(Non-visual logic composition)的终极方案。它解决了过去高阶组件(HOC)和 Render Props 导致的"嵌套地狱(Wrapper Hell)"问题。

如果你的组合目的仅仅是复用状态和逻辑,而不是复用 UI 结构,请毫不犹豫地选择 Custom Hooks。

jsx 复制代码
// 抽象出的计数器逻辑
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)
  const increment = () => setCount(c => c + 1)
  return { count, increment }
}

// 组件 A 使用该逻辑
function CounterComponent() {
  const { count, increment } = useCounter(10)
  return <button onClick={increment}>Count: {count}</button>
}

💡 总结与选型指南

在实际开发中,你可以根据以下逻辑来选择最适合的组合模式:

需求场景 推荐模式 核心思路
基础包装(如给一组组件加个通用的外框/边框) props.children 最简单的上下嵌套
复杂页面结构布局(如固定头部、侧边栏、主内容区) 插槽模式(Props Injection) 通过命名 props 传入组件
组件群状态共享(如 Tabs、Dropdown、Accordion) 复合组件(Compound Components) 外层用 Context,内层语义化组装
纯逻辑复用(如请求数据、监听网络、表单验证) Custom Hooks 剥离 UI,只组合和共享逻辑状态
相关推荐
java1234_小锋4 小时前
【技术专题】HTML5 网页技术 - HTML5 Canvas 基础以及动画入门
前端·html·html5
知了学历孙老师7 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
ShineWinsu9 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说9 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
志尊宝11 小时前
AI原生前端开发(009):AI辅助性能分析与优化建议
人工智能·react.js·ai-native
程序员老赵11 小时前
Docker 部署青龙面板:轻松搭建定时任务管理平台
前端·docker·开源
Swot11 小时前
Asciidoctor 公式数字下标错位问题:原因分析与解决方案
前端
Swot11 小时前
leo-editor 中将 Markdown 内容转成 asciidoc 格式
前端
狒狒AI12 小时前
Remotion / GSAP+Three / HyperFrames / Skia 四栈实测:代码渲染知识视频的选栈方法(附开源技能)
前端·人工智能·aigc
修炼的dance12 小时前
浏览器里导出H.265,8.5秒和55.5秒差在硬件
前端