# 高阶组件HOC

7 min read
Table of Contents

高阶组件 HOC 是一个函数,它接受一个组件并返回一个新组件。普通组件是将 props 转换成 UI,而高阶组件将组件转换成另一个组件。

const EnhancedComponent = higherOrderComponent(WrappedComponent)

使用原因和实现

高阶组件主要可以帮忙解决以下三个方面的问题:

  1. 抽取重复代码,实现组件复用。如页面复用。
  2. 条件渲染,控制组件的渲染逻辑(渲染劫持)。如权限控制。
  3. 捕获/劫持被处理组件的生命周期。如组件渲染性能跟踪,日志打点。

主要实现有两种方式

  1. 属性代理(Props Proxy)
  2. 反向继承(Inheritance Inversion)

属性代理

属性代理本质是使用组合的方式,通过将组件包装在容器组件中实现功能。

// 返回一个函数组件
function hoc(WrappedComponent) {
const newProps = { type: 'HOC' }
return (props) => <WrappedComponent {...props} {...newProps} />
}
// 返回一个类组件
function hoc(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = { type: 'HOC' }
return <WrappedComponent {...this.props} {...newProps} />
}
}
}

代码示例:codesandbox

实例应用 - 页面复用

横切关注点(Cross-Cutting Concern),即系统中的部分功能在多个模块中出现。所谓横切关注点就是页面复用。

假设有一个 CommentList 评论列表组件和一个 BlogPost 博客帖子组件。CommentList 组件订阅外部数据源,用以渲染评论列表。BlogPost 订阅单个博客帖子,用以渲染博客。

对于经常复用的逻辑,我们可以进行抽象,在一个地方定义这个逻辑,并且在许多组件之间共享它。对于上面的例子,我们可以编写一个创建组件的函数,该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。我们定义函数 withSubscription

第一个参数是被包装的组件。第二个参数通过 DataSource 和当前的 props 返回我们需要的数据。

import DataSource from '..'
// hoc
function withSubscription(WrappedComponent, selectData) {
return (props) => {
const [data, setData] = useState(selectData(DataSource, props))
function handlechange() {
setData(selectdata(Datasource, props))
}
useEffect(() => {
Datasource.addchangelistener(handlechange)
return () => {
Datasource.removechangelistener(handlechange)
}
}, [])
return <wrappedcomponent data={data} {...props} />
}
}
const Commentlistwithsubscription = withsubscription(Commentlist, (datasource) =>
Datasource.getcomments(),
)
const BlogPostWithSubscrption = withSubscription(BlogPost, (DataSource, props) =>
DataSource.getBlogPost(props.id),
)

实例应用 - 权限控制

权限控制其实就是利用高阶组件的条件渲染来控制渲染的组件。

function authWrapper(WrappedComponent) {
// 通过其他方法路径获取权限
const isAuth = getAuth()
if (!isAuth) return null
return (props) => <WrappedComponent {...props} />
}

实例应用 - 类组件使用 Hooks

在有继承的或者部分复杂的类组件,需要使用 hooks 时,可以使用 HOC 来包裹成函数组件。

App.tsx
import useName from './useName'
class Greeting extends React.Component<{ name: string }> {
render() {
return (
<h1>
Hello <span>{this.props.name}</span>
</h1>
)
}
}
const WrapperGreeting = () => {
const [name] = useName('chen')
return <Greeting name={name} />
}
export default function App() {
return (
<div className="App">
<WrapperGreeting />
</div>
)
}
useName.ts
import { useEffect, useState } from 'react'
const useName = (n: string) => {
const [name, setName] = useState('')
useEffect(() => {
setName(n)
}, [n])
return [name]
}
export default useName

https://codesandbox.io/s/class-component-use-hooks-cgmoo1

不要改变原始组件,使用组合

不要在 HOC 中修改组件原型,这样子会产生一些不良后果,HOC 不应该修改传入的组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能

// 错误的使用方式
function logPrpos(InputComponent) {
InputComponent.prototype.componentDidUpdate = function (prevProps) {
console.log('Current props: ', this.props)
console.log('Previous props: ', prevProps)
}
// 返回原始的 input 组件,暗示它已经被修改。
return InputComponent
}
// 每次调用 logPrpos 时,增强组件都会有 log 输出
const EnhancedComponent = logPrpos(InputComponent)
// 使用组合的方式
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props)
console.log('Previous props: ', prevProps)
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改
return <WrappedComponent {...this.props} />
}
}
}

三个约定

  • 约定:将不相关的 props 传递给被包裹的组件

    这个约定保证了 HOC 的灵活性以及可复用性。HOC 为组件添加特性,HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含类似下面的 render 方法

    render() {
    // 过滤掉非此 HOC 且额外不要进行透传的 props
    const { extraProp, ...passThroughProps } = this.props;
    // 将 props 注入到被包装的组件中。
    // 通常为 state 的值或者实例方法。
    const injectedProps = someStateOrInstanceMethod;
    // 将 props 传递给被包裹的组件
    return (
    <wrappedcomponent injectedProps={injectedProps} {...passThroughProps} />
    );
    }
  • 约定:最大化可组合性

  • 约定:包装显示名称以便轻松调试

注意事项

  • 不要在 render 方法中使用 HOC

  • 务必复制静态方法

  • Refs 不会被传递

参考链接


More Posts

# 虚拟DOM

2 min read

Virtual DOM,虚拟 DOM 是 React 创建的一个树型自定义对象来代表真实 DOM。使得 React 可以快速操作这些对象,而不需实际接触到真实的 DOM 或者通过使用 DOM API。最后,当需要渲染的时候,可以使用这个虚拟 DOM 计算得出真实的 DOM,并把它渲染到浏览器上。

# CSS实现图形

2 min read

有一些形状可以通过纯 CSS 实现图形,如下拉三角型,下拉箭头等。。。