react-learn-hoc
4 min read
Table of Contents
React 高阶组件(HOC)详解
什么是高阶组件?
高阶组件(Higher-Order Component,HOC) 是 React 中用于复用组件逻辑的一种高级技巧。HOC 本质上是一个函数,它接收一个组件作为参数,并返回一个新的增强组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent)基础示例
1. 简单的 HOC - 添加日志功能
// 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
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. 条件渲染
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. 数据获取和订阅
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
// 多个 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>
)
}最佳实践和注意事项
✅ 该做的:
- 透传不相关的 Props
function withSomething(WrappedComponent) { return function (props) { const extraProp = 'value' // 将所有 props 传递给被包装组件 return <WrappedComponent extraProp={extraProp} {...props} /> }}- 最大化组合性 - 使用 displayName
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'}❌ 不要做的:
- 不要在 render 方法中使用 HOC
// ❌ 错误render() { // 每次渲染都会创建新组件,导致性能问题 const EnhancedComponent = withSomething(MyComponent); return <EnhancedComponent />;}
// ✅ 正确const EnhancedComponent = withSomething(MyComponent);class App extends React.Component { render() { return <EnhancedComponent />; }}- 不要修改原始组件
// ❌ 错误function withMutation(WrappedComponent) { WrappedComponent.prototype.componentDidUpdate = function () { // 修改了原始组件 } return WrappedComponent}
// ✅ 正确 - 使用组合function withEnhancement(WrappedComponent) { return class extends React.Component { componentDidUpdate() { // 新增行为 } render() { return <WrappedComponent {...this.props} /> } }}- 静态方法需要手动复制
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:
// HOC 方式const EnhancedComponent = withUserData(MyComponent)
// Hooks 方式 (更推荐)function MyComponent() { const userData = useUserData() // 自定义 Hook return <div>{userData.name}</div>}Hooks 的优势:
- 更简洁的代码
- 避免”包装地狱”
- 更好的 TypeScript 支持
- 逻辑更容易追踪
评论 (0)
请先登录后再发表评论