Featured image of post React 高级模式:HOC / Slot / Server Components / Actions 演进

React 高级模式:HOC / Slot / Server Components / Actions 演进

从 HOC(高阶组件)到 Slot(插槽)、从 RSC(React Server Components)到 Actions API——React 5 年来的「组合 + 渲染」模式演进全景。一篇打通组件复用、服务端渲染、异步状态管理的所有关键概念。

为什么写这篇:React 的"组合(composition)“思想从 props 出发,演化出 HOC、Render Props、Slot、Compound Components 等模式;进入 React 19 又迎来 Server Components 与 Actions 的"服务端 + 异步"革命。本文是 React 高级模式的演进史。

适用读者:想深入理解 React 组件设计的中高级前端;准备面试 React 资深岗的工程师;要选型 RSC/SSR/CSR 架构的技术负责人。

前置知识:掌握 Hooks、JSX、组件生命周期。

目录

  1. React 组件设计的"组合"哲学
  2. HOC:高阶组件
  3. Slot:插槽模式(内容分发)
  4. RSC:React Server Components
  5. 服务端组件 vs 客户端组件的边界
  6. Actions API:声明式异步操作
  7. 设计可复用、可维护组件的核心原则
  8. 模式选型:什么时候用什么

1. React 组件设计的"组合"哲学

React 的核心设计原则之一是 “组合优于继承(composition over inheritance)”——UI 由小组件拼装成大组件,而不是用类继承扩展基类。

React 提供的组合工具从早到晚:

工具时代解决问题
props / children全时代最基础的"传值 + 插槽”
HOC(高阶组件)2015-2019跨组件复用逻辑(Hooks 出现前)
Render Props2017-2020函数式 children 传值
Hooks2019+函数组件的状态、副作用、复用
Compound Components2018+父组件 + 子组件隐式共享 state
Server Components2023+服务端渲染 + 零客户端 JS
Actions2024+声明式表单 + 异步操作

本文挑出 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(windowdocument
限制不能绑定 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 />
跨组件复用逻辑自定义 HookuseAuth() / useFetch()
老的横切关注点HOC(仅维护)withAuth / connect
服务端取数 + 静态内容RSC博客文章页 / 列表页
表单 + 异步提交Actions评论框 / 登录 / 搜索
需要乐观更新useOptimistic点赞 / 收藏

新项目默认

  1. RSC(Next.js App Router)做骨架
  2. 客户端组件做交互
  3. 自定义 Hook 抽业务逻辑
  4. 复杂表单用 Actions
  5. HOC 已成历史

小结

React 5 年的演进路径:

  1. 2015-2019:HOC 时代,class 组件 + HOC
  2. 2019-2024:Hooks 时代,函数组件 + 自定义 Hook
  3. 2024+:RSC + Actions 时代,服务端组件 + 声明式异步

新项目的默认架构

  • 服务端组件做 80% 的渲染
  • 客户端组件只做交互
  • 表单用 Actions
  • 状态用 Hook + Zustand/Redux
  • 跨组件逻辑用自定义 Hook

下一步:学 Next.js 14+ App Router——RSC + Actions 的生产级实践。或深入 React 19 Compiler——自动 memoization,让 React 性能优化"零成本"。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计