为什么写这篇:React 的"组合(composition)“思想从 props 出发,演化出 HOC、Render Props、Slot、Compound Components 等模式;进入 React 19 又迎来 Server Components 与 Actions 的"服务端 + 异步"革命。本文是 React 高级模式的演进史。
适用读者:想深入理解 React 组件设计的中高级前端;准备面试 React 资深岗的工程师;要选型 RSC/SSR/CSR 架构的技术负责人。
前置知识:掌握 Hooks、JSX、组件生命周期。
目录
- React 组件设计的"组合"哲学
- HOC:高阶组件
- Slot:插槽模式(内容分发)
- RSC:React Server Components
- 服务端组件 vs 客户端组件的边界
- Actions API:声明式异步操作
- 设计可复用、可维护组件的核心原则
- 模式选型:什么时候用什么
1. React 组件设计的"组合"哲学
React 的核心设计原则之一是 “组合优于继承(composition over inheritance)”——UI 由小组件拼装成大组件,而不是用类继承扩展基类。
React 提供的组合工具从早到晚:
| 工具 | 时代 | 解决问题 |
|---|
| props / children | 全时代 | 最基础的"传值 + 插槽” |
| HOC(高阶组件) | 2015-2019 | 跨组件复用逻辑(Hooks 出现前) |
| Render Props | 2017-2020 | 函数式 children 传值 |
| Hooks | 2019+ | 函数组件的状态、副作用、复用 |
| Compound Components | 2018+ | 父组件 + 子组件隐式共享 state |
| Server Components | 2023+ | 服务端渲染 + 零客户端 JS |
| Actions | 2024+ | 声明式表单 + 异步操作 |
本文挑出 HOC、Slot、RSC、Actions 四个深入讲。
2. HOC:高阶组件
2.1 什么是 HOC
HOC 是一个函数,接收一个组件作为参数,返回一个新的组件——源自函数式编程的"高阶函数"。
1
2
3
4
5
6
7
8
9
10
11
| const withLogger = (WrappedComponent) => {
return function NewComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`)
}, [])
return <WrappedComponent {...props} />
}
}
// 用法
const UserPageWithLogger = withLogger(UserPage)
|
2.2 HOC 解决什么问题
| 场景 | 例子 |
|---|
| 逻辑复用 | 数据获取、订阅、权限控制、日志记录——多个组件都要 |
| 横切关注点 | 认证、i18n、主题切换——与业务无关但重复出现 |
| Props 注入 | 注入 currentUser / theme / i18n 给所有被包装组件 |
| 渲染劫持 | 渲染前后添加元素、修改 props |
2.3 经典案例:withAuth
1
2
3
4
5
6
7
8
9
10
11
12
| function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth()
if (loading) return <Spinner />
if (!user) return <Navigate to="/login" />
return <WrappedComponent {...props} user={user} />
}
}
const DashboardPage = withAuth(Dashboard)
|
2.4 HOC 的优缺点
| 优点 | 缺点 |
|---|
| 复用逻辑,不修改原组件 | 嵌套地狱(5 层 HOC 包裹的组件调试噩梦) |
| 隔离横切关注点 | props 命名冲突(多个 HOC 注入同名 prop) |
| 兼容老代码(class + function 都可包) | 静态类型推断难(TypeScript 难判断返回组件的 props) |
2.5 vs Hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // ❌ HOC:嵌套 3 层
const Enhanced = withAuth(withTheme(withI18n(UserPage)))
// ✅ 自定义 Hook:扁平化
function usePageContext() {
const { user } = useAuth()
const { theme } = useTheme()
const { t } = useI18n()
return { user, theme, t }
}
function UserPage() {
const ctx = usePageContext()
// ...
}
|
结论:Hooks 出现后,新项目基本不用 HOC。但维护老代码、与第三方库集成(如 redux 的 connect)时仍会见到。
3. Slot:插槽模式(内容分发)
3.1 什么是 Slot
Slot 允许父组件在子组件内部的"预定义位置"插入任意内容。Vue 有 <slot> 标签,Web Components 有 <slot name="header">,React 通过 children prop + 命名 prop 实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 经典:通用卡片
interface CardProps {
title: string
footer?: React.ReactNode
children: React.ReactNode
}
function Card({ title, footer, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
)
}
// 用法
<Card
title="用户信息"
footer={<button>编辑</button>}
>
<p>姓名:张三</p>
<p>年龄:20</p>
</Card>
|
3.2 命名 Slot(多个插槽)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| interface LayoutProps {
header: React.ReactNode
sidebar: React.ReactNode
children: React.ReactNode
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
)
}
// 用法
<Layout
header={<TopNav />}
sidebar={<SideMenu />}
>
<PageContent />
</Layout>
|
3.3 Slot 的优势
| 优势 | 说明 |
|---|
| 灵活性 | 父组件完全控制插槽内容(文本 / JSX / 组件都可以) |
| 职责分离 | 子组件管"外壳",父组件管"内容" |
| 可复用 | 通用 Layout/Card/Modal 都能这么写 |
| 避免 props 地狱 | 不用 10 个 props 传内容片段 |
3.4 条件 Slot
1
2
3
4
5
6
7
8
9
| function Card({ title, children, showFooter, footer }) {
return (
<div>
<h3>{title}</h3>
{children}
{showFooter && footer && <div>{footer}</div>}
</div>
)
}
|
4. RSC:React Server Components
4.1 什么是 RSC
React Server Components(RSC)是 React 19 引入的新范式——让组件可以只在服务端运行,零 JS 发送到客户端。
不是要"取代"客户端组件,而是"补充":根据组件特性(是否需要交互、是否需要频繁更新、是否涉及敏感数据)选择在服务端还是客户端渲染。
4.2 三大痛点
痛点 1:客户端 JS 包体积巨大
1
2
3
4
5
| 传统 CSR:
- React + ReactDOM:140KB
- 状态管理库:50KB
- 业务代码 + 第三方库:500KB
- 全部 → 浏览器
|
RSC 把"无需交互"的服务端组件整个留在服务器——这部分代码和依赖永远不会到客户端。
痛点 2:低效的客户端数据获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // ❌ 传统 CSR:useEffect 串行请求
function UserProfile({ id }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${id}`).then(r => r.json()).then(setUser)
}, [id])
// 还要写 loading / error / cleanup...
}
// ✅ RSC:async 服务端组件
async function UserProfile({ id }) {
const user = await db.user.findUnique({ where: { id } }) // 直接 DB
return <div>{user.name}</div>
}
|
优势:
- 消除"瀑布式请求"——服务端并行取数
- 数据靠近数据源——直接调数据库
- 简化代码——不用 useState/useEffect/cleanup
- 减少网络延迟——不走 HTTP API
痛点 3:敏感信息暴露
CSR 模式下,所有 JS 都被发到浏览器——API key、数据库查询语句、SSO 回调 URL 全部可见。
RSC 把这些留在服务端——客户端只看到渲染后的 UI 描述。
4.3 RSC 的核心思想
“将渲染和数据获取的计算移动到最合适的地方——服务器,同时保持 React 组件模型的开发体验。”
5. 服务端组件 vs 客户端组件的边界
5.1 服务端组件(RSC)
| 维度 | 描述 |
|---|
| 运行环境 | 仅在服务端执行 |
| JS 体积贡献 | 零(代码和依赖不会发到客户端) |
| 能力 | 直接访问数据库、文件系统、内部 API、敏感操作 |
| 限制 | 不能用 useState / useReducer / useEffect / useLayoutEffect |
| 限制 | 不能用浏览器 API(window、document) |
| 限制 | 不能绑定 onClick / onChange 等事件 |
| 适用 | 静态内容、读 DB、读文件、保护敏感逻辑 |
5.2 客户端组件(RCC)
| 维度 | 描述 |
|---|
| 运行环境 | 客户端浏览器 |
| JS 体积 | 完整 JS 包 + 依赖 |
| 能力 | useState/useEffect/浏览器 API/事件处理 |
| 限制 | 不能直接调数据库 / 读文件系统 |
| 限制 | 不能直接 import 服务端组件 |
| 适用 | 表单、按钮、状态管理、WebSocket、地理位置 |
5.3 协作模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // app/page.tsx (Next.js 14+ 默认 RSC)
import { db } from '@/lib/db'
import LikeButton from './LikeButton' // 客户端组件
// 服务端组件:直接读 DB
export default async function Page() {
const post = await db.post.findFirst()
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 嵌套客户端组件——子组件的 RSC 部分仍是零 JS */}
<LikeButton postId={post.id} initialCount={post.likes} />
</article>
)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // LikeButton.tsx
'use client' // ← 标记为客户端组件
import { useState } from 'react'
export default function LikeButton({ postId, initialCount }) {
const [count, setCount] = useState(initialCount)
return (
<button onClick={async () => {
setCount(c => c + 1) // 乐观更新
await fetch(`/api/posts/${postId}/like`, { method: 'POST' })
}}>
❤️ {count}
</button>
)
}
|
5.4 何时该用 RSC
1
2
3
4
5
6
7
8
9
| 问自己:这个组件需要用户交互吗?
├─ 否 → RSC(默认)
│
├─ 是(按钮点击 / 表单 / 动画 / 状态管理)
│ └─ 客户端组件('use client')
│
└─ 数据来源是?
├─ 数据库 / 文件系统 / 内部 API → RSC(直接在服务端取)
└─ 第三方 API(用 useEffect)→ 客户端组件
|
黄金法则:默认所有组件都是 RSC,只有需要交互的才标 'use client'。
6. Actions API:声明式异步操作
6.1 传统痛点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // ❌ 传统表单:5 个 useState、3 个 effect、手动管理 loading/error
function OldForm() {
const [title, setTitle] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
await fetch('/api/save', { method: 'POST', body: JSON.stringify({ title }) })
setSuccess(true)
} catch (e) {
setError(e)
} finally {
setLoading(false)
}
}
// ... 80 行
}
|
6.2 Actions:声明式解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // ✅ React 19 Actions
async function saveAction(prevState, formData) {
await fetch('/api/save', { method: 'POST', body: formData })
return { ok: true }
}
function NewForm() {
const [state, formAction, isPending] = useActionState(saveAction, { ok: false })
return (
<form action={formAction}>
<input name="title" />
<button disabled={isPending}>{isPending ? '保存中...' : '保存'}</button>
{state.ok && <p>已保存</p>}
</form>
)
}
|
6.3 三个核心 Hook
1
2
3
4
5
6
7
8
| // useActionState:表单状态封装
const [state, action, isPending] = useActionState(actionFn, initialState)
// useFormStatus:在表单内部组件中读父表单状态
const { pending, data, method } = useFormStatus()
// useOptimistic:乐观更新
const [optimisticState, addOptimistic] = useOptimistic(actualState, reducer)
|
6.4 乐观更新示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| async function addCommentAction(prev, formData) {
const text = formData.get('text')
// 模拟 API 慢
await new Promise(r => setTimeout(r, 1000))
return [...prev, { id: Date.now(), text }]
}
function Comments() {
const [comments, dispatch, isPending] = useActionState(addCommentAction, [])
const [optimistic, addOptimistic] = useOptimistic(comments)
return (
<form action={async (formData) => {
addOptimistic({ id: Date.now(), text: formData.get('text') }) // 立即显示
dispatch(formData) // 后台提交
}}>
<input name="text" />
<button>提交</button>
<ul>
{optimistic.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
</form>
)
}
|
效果:用户提交评论立即显示,后台异步落库——体感零延迟。
7. 设计可复用、可维护组件的核心原则
7.1 单一职责原则(SRP)
一个组件只做一件事,并把它做好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // ❌ 反例:UI + 数据 + 业务全在一个组件
function UserPage() {
const [users, setUsers] = useState([])
const [filter, setFilter] = useState('')
useEffect(() => { fetch('/api/users').then(/* ... */) }, [])
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<table>
{users.filter(u => u.name.includes(filter)).map(u => (
<tr key={u.id}><td>{u.name}</td><td>{u.email}</td></tr>
))}
</table>
</div>
)
}
// ✅ 正例:拆分
function useUsers() { /* 数据获取 */ return { users, loading } }
function UserFilter({ value, onChange }) { return <input ... /> }
function UserTable({ users }) { return <table>...</table> }
function UserPage() {
const { users } = useUsers()
const [filter, setFilter] = useState('')
return (
<>
<UserFilter value={filter} onChange={setFilter} />
<UserTable users={users.filter(u => u.name.includes(filter))} />
</>
)
}
|
7.2 关注点分离(SoC)
| 层 | 职责 |
|---|
| 数据层 | Redux / Zustand / React Query |
| 逻辑层 | 自定义 Hook(封装业务规则) |
| 视图层 | 纯函数组件(只负责渲染) |
| 样式 | CSS Modules / Tailwind / styled-components |
7.3 松散耦合 & 高内聚
- 松散耦合:组件之间通过 props 通信,不用 ref 互相穿透
- 高内聚:相关逻辑(state + effect + render)放在一起,不要"散弹式修改"
7.4 封装
- 内部状态私有化(不暴露
setX 出去) - 清晰的 props 接口(TypeScript interface 明确类型 + 必填/可选)
- 避免"泄露"内部实现(不要
props.children 之外传 DOM ref 出去)
7.5 实践清单
- ✅ 函数组件 + Hooks(不要 class)
- ✅ TypeScript 定义 props 接口
- ✅ camelCase 命名 prop、PascalCase 组件
- ✅ 区分受控/非受控组件
- ✅ 写测试(React Testing Library)
- ✅ 充分文档(Storybook)
- ✅ 考虑可访问性(A11y):语义化 HTML、ARIA、键盘导航
- ✅ 性能意识:React.memo、useCallback、懒加载
8. 模式选型:什么时候用什么
| 场景 | 模式 | 例子 |
|---|
| 简单传值 | props | <Button color="red" /> |
| 简单插槽 | children | <Card>任意内容</Card> |
| 多插槽 | 命名 prop | <Layout header sidebar /> |
| 跨组件复用逻辑 | 自定义 Hook | useAuth() / useFetch() |
| 老的横切关注点 | HOC(仅维护) | withAuth / connect |
| 服务端取数 + 静态内容 | RSC | 博客文章页 / 列表页 |
| 表单 + 异步提交 | Actions | 评论框 / 登录 / 搜索 |
| 需要乐观更新 | useOptimistic | 点赞 / 收藏 |
新项目默认:
- RSC(Next.js App Router)做骨架
- 客户端组件做交互
- 自定义 Hook 抽业务逻辑
- 复杂表单用 Actions
- HOC 已成历史
小结
React 5 年的演进路径:
- 2015-2019:HOC 时代,class 组件 + HOC
- 2019-2024:Hooks 时代,函数组件 + 自定义 Hook
- 2024+:RSC + Actions 时代,服务端组件 + 声明式异步
新项目的默认架构:
- 服务端组件做 80% 的渲染
- 客户端组件只做交互
- 表单用 Actions
- 状态用 Hook + Zustand/Redux
- 跨组件逻辑用自定义 Hook
下一步:学 Next.js 14+ App Router——RSC + Actions 的生产级实践。或深入 React 19 Compiler——自动 memoization,让 React 性能优化"零成本"。
参考资料