在 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,只组合和共享逻辑状态 |