react-learn-hoc

4 min read
Table of Contents

React 高阶组件(HOC)详解

什么是高阶组件?

高阶组件(Higher-Order Component,HOC) 是 React 中用于复用组件逻辑的一种高级技巧。HOC 本质上是一个函数,它接收一个组件作为参数,并返回一个新的增强组件。

javascript
const EnhancedComponent = higherOrderComponent(WrappedComponent)

基础示例

1. 简单的 HOC - 添加日志功能

javascript
// withLogger.js - 为组件添加日志功能
function withLogger(WrappedComponent) {
return function LoggerComponent(props) {
console.log('组件渲染了,props:', props)
return <WrappedComponent {...props} />
}
}
// 使用
function MyButton({ onClick, label }) {
return <button onClick={onClick}>{label}</button>
}
const MyButtonWithLogger = withLogger(MyButton)

2. 实用的 HOC - 权限控制

管理员页面
没有权限访问
import React from 'react'

enum Role {
  ADMIN = 'admin',
  USER = 'user',
}

const withAuthorization = (role: Role) => (Component: React.FC) => {
  const isAuth = (role: Role) => {
    return role === Role.ADMIN
  }
  return (props: any) => {
    if (isAuth(role)) {
      return <Component {...props} />
    } else {
      return (
        <div className="bg-amber-500 text-[#242424] flex items-center justify-center p-4 w-50 h-full">
          没有权限访问
        </div>
      )
    }
  }
}

const AdminComponent = withAuthorization(Role.ADMIN)(() => {
  return (
    <div className="bg-[#5de551] text-[#242424] flex items-center justify-center w-50 h-full">
      管理员页面
    </div>
  )
})
const UserComponent = withAuthorization(Role.USER)(() => {
  return (
    <div className="bg-amber-500 text-[#242424] flex items-center justify-center p-4 w-50 h-full">
      用户页面
    </div>
  )
})

export default function App() {
  return (
    <>
      <div className="w-100 h-40 flex flex-row gap-4]">
        <AdminComponent a="1" />
        <UserComponent a="1" />
      </div>
    </>
  )
}

3. 注入 Props 的 HOC

jswithUserData.js
function withUserData(WrappedComponent) {
return function UserDataComponent(props) {
const [userData, setUserData] = React.useState(null)
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
fetchUserData().then((data) => {
setUserData(data)
setLoading(false)
})
}, [])
// 注入额外的 props
return <WrappedComponent {...props} userData={userData} isLoading={loading} />
}
}
// 使用
function UserProfile({ userData, isLoading, customProp }) {
if (isLoading) return <div>加载中...</div>
return (
<div>
<h1>{userData.name}</h1>
<p>{customProp}</p>
</div>
)
}
const EnhancedUserProfile = withUserData(UserProfile)

常见应用场景

1. 条件渲染

javascript
function withConditionalRender(WrappedComponent, condition) {
return function ConditionalComponent(props) {
if (!condition(props)) {
return null // 或者返回占位符
}
return <WrappedComponent {...props} />
}
}
// 使用
const VisibleOnlyIfAdmin = withConditionalRender(
AdminPanel,
(props) => props.user.role === 'admin',
)

2. 数据获取和订阅

javascript
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props)
this.state = {
data: selectData(DataSource, props),
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange = () => {
this.setState({
data: selectData(DataSource, this.props),
})
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}

3. 组合多个 HOC

javascript
// 多个 HOC 组合使用
const enhance = compose(withAuth, withLogger, withUserData)
const EnhancedComponent = enhance(MyComponent)
// 或者链式调用
const EnhancedComponent = withAuth(withLogger(withUserData(MyComponent)))

4. 追踪事件

import React, { useEffect } from 'react'

/**
 * 追踪服务
 * 负责收集和发送用户行为数据到服务器
 */
const trackService = {
  /**
   * 发送追踪事件
   * @param trackType - 事件类型(如 'button-MOUNT', 'button-click' 等)
   * @param data - 可选的附加数据,可以是任意类型
   *
   * 使用 navigator.sendBeacon 发送数据,这是一个专门用于发送分析数据的 API,
   * 即使在页面卸载时也能可靠地发送数据,不会阻塞页面关闭
   */
  sendEvent: <T,>(trackType: string, data: T = null as T) => {
    // 构建事件数据对象,包含时间戳、事件类型、自定义数据、URL 和用户代理
    const eventData = {
      timestamp: new Date().toISOString(), // ISO 格式的时间戳
      trackType, // 事件类型
      data, // 自定义数据
      url: window.location.href, // 当前页面 URL
      ua: navigator.userAgent, // 浏览器用户代理信息
    }
    // 使用 sendBeacon 发送数据,即使页面关闭也能发送
    navigator.sendBeacon('https://api.exmaple.com/track', JSON.stringify(eventData))
  },
}

/**
 * 高阶组件(HOC - Higher-Order Component)
 * 用于为组件添加追踪功能
 *
 * @template T - 被包装组件的 props 类型
 * @param Component - 需要添加追踪功能的原始组件
 * @param trackType - 追踪类型标识符,用于区分不同的组件(如 'button', 'modal' 等)
 * @returns 返回一个新的组件,该组件会自动追踪挂载/卸载事件,并提供 trackEvent 方法
 *
 * 工作原理:
 * 1. 接收一个组件和追踪类型作为参数
 * 2. 返回一个新的函数组件
 * 3. 新组件会在挂载时发送 MOUNT 事件,卸载时发送 UNMOUNT 事件
 * 4. 为原始组件注入 trackEvent 方法,使其可以发送自定义追踪事件
 */
const withTrack = <T,>(Component: React.ComponentType<T>, trackType: string) => {
  // 返回一个新的函数组件
  return (props: Omit<T, 'trackEvent'>) => {
    // 使用 useEffect 监听组件的生命周期
    useEffect(() => {
      // 组件挂载时发送追踪事件
      trackService.sendEvent(`${trackType}-MOUNT`)

      // 返回清理函数,在组件卸载时执行
      return () => {
        // 组件卸载时发送追踪事件
        trackService.sendEvent(`${trackType}-UNMOUNT`)
      }
    }, []) // 空依赖数组,确保只在挂载和卸载时执行

    /**
     * 创建 trackEvent 方法,供被包装的组件使用
     * @param eventType - 事件类型(如 'click', 'hover' 等)
     * @param data - 事件相关的数据
     */
    const trackEvent = (eventType: string, data: any) => {
      // 将 trackType 和 eventType 组合成完整的事件类型标识
      trackService.sendEvent(`${trackType}-${eventType}`, data)
    }

    // 渲染原始组件,并注入 trackEvent 方法
    // 使用类型断言确保 props 类型正确
    return <Component {...(props as T)} trackEvent={trackEvent} />
  }
}

/**
 * 示例按钮组件
 * 展示如何使用通过 HOC 注入的 trackEvent 方法
 *
 * @param trackEvent - 由 HOC 注入的追踪方法
 */
const Button = ({
  trackEvent,
}: {
  trackEvent: (eventType: string, data: any) => void
}) => {
  /**
   * 处理按钮点击事件
   * 在点击时发送追踪数据,记录点击位置等信息
   */
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    // 调用 trackEvent 发送追踪事件
    // 事件类型为 'click',数据包含按钮名称和点击坐标
    trackEvent(e.type, {
      name: 'button',
      clientX: e.clientX, // 鼠标点击的 X 坐标
      clientY: e.clientY, // 鼠标点击的 Y 坐标
    })
  }
  return (
    <button onClick={handleClick} onDoubleClick={handleClick}>
      Click me
    </button>
  )
}

/**
 * 使用 HOC 包装 Button 组件,添加追踪功能
 *
 * 使用方式:
 * 1. 调用 withTrack,传入原始组件和追踪类型标识
 * 2. 返回的新组件会自动:
 *    - 在挂载时发送 'button-MOUNT' 事件
 *    - 在卸载时发送 'button-UNMOUNT' 事件
 *    - 为 Button 组件注入 trackEvent 方法
 * 3. Button 组件可以通过 trackEvent 发送自定义事件(如 'button-click')
 */
const ButtonWithTrack = withTrack(Button, 'button')

/**
 * 应用主组件
 * 展示如何使用带追踪功能的按钮组件
 */
export default function App() {
  return (
    <div>
      {/* 使用带追踪功能的按钮,会自动记录所有相关事件 */}
      <ButtonWithTrack />
    </div>
  )
}

最佳实践和注意事项

该做的:

  1. 透传不相关的 Props
javascript
function withSomething(WrappedComponent) {
return function (props) {
const extraProp = 'value'
// 将所有 props 传递给被包装组件
return <WrappedComponent extraProp={extraProp} {...props} />
}
}
  1. 最大化组合性 - 使用 displayName
javascript
function withSomething(WrappedComponent) {
function WithSomething(props) {
return <WrappedComponent {...props} />
}
// 设置 displayName 便于调试
WithSomething.displayName = `WithSomething(${getDisplayName(WrappedComponent)})`
return WithSomething
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

不要做的:

  1. 不要在 render 方法中使用 HOC
javascript
// ❌ 错误
render() {
// 每次渲染都会创建新组件,导致性能问题
const EnhancedComponent = withSomething(MyComponent);
return <EnhancedComponent />;
}
// ✅ 正确
const EnhancedComponent = withSomething(MyComponent);
class App extends React.Component {
render() {
return <EnhancedComponent />;
}
}
  1. 不要修改原始组件
javascript
// ❌ 错误
function withMutation(WrappedComponent) {
WrappedComponent.prototype.componentDidUpdate = function () {
// 修改了原始组件
}
return WrappedComponent
}
// ✅ 正确 - 使用组合
function withEnhancement(WrappedComponent) {
return class extends React.Component {
componentDidUpdate() {
// 新增行为
}
render() {
return <WrappedComponent {...this.props} />
}
}
}
  1. 静态方法需要手动复制
javascript
import hoistNonReactStatics from 'hoist-non-react-statics'
function withSomething(WrappedComponent) {
class WithSomething extends React.Component {
/* ... */
}
// 复制静态方法
hoistNonReactStatics(WithSomething, WrappedComponent)
return WithSomething
}

HOC vs Hooks

现代 React 开发中,Hooks(特别是自定义 Hooks)在很多场景下可以替代 HOC:

javascript
// HOC 方式
const EnhancedComponent = withUserData(MyComponent)
// Hooks 方式 (更推荐)
function MyComponent() {
const userData = useUserData() // 自定义 Hook
return <div>{userData.name}</div>
}

Hooks 的优势:

  • 更简洁的代码
  • 避免”包装地狱”
  • 更好的 TypeScript 支持
  • 逻辑更容易追踪

后记

My avatar

感谢你读到这里。

这座小站更像一份持续维护的“终端笔记”:记录我解决问题的过程,也记录走过的弯路。

如果这篇内容对你有一点点帮助:

  • 点个赞 / 收藏一下,方便你下次回来继续翻
  • 欢迎在评论区补充你的做法(或者指出我的疏漏)
  • 想持续收到更新:可以订阅 RSS(在页面底部)

我们下篇见。


More Posts

评论

评论 (0)

请先登录后再发表评论

加载中...