# 高阶组件HOC
Table of Contents
高阶组件 HOC 是一个函数,它接受一个组件并返回一个新组件。普通组件是将 props 转换成 UI,而高阶组件将组件转换成另一个组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent)使用原因和实现
高阶组件主要可以帮忙解决以下三个方面的问题:
- 抽取重复代码,实现组件复用。如页面复用。
- 条件渲染,控制组件的渲染逻辑(渲染劫持)。如权限控制。
- 捕获/劫持被处理组件的生命周期。如组件渲染性能跟踪,日志打点。
主要实现有两种方式
- 属性代理(Props Proxy)
- 反向继承(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 '..'// hocfunction 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 来包裹成函数组件。
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> )}import { useEffect, useState } from 'react'
const useName = (n: string) => { const [name, setName] = useState('') useEffect(() => { setName(n) }, [n]) return [name]}
export default useNamehttps://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 且额外不要进行透传的 propsconst { extraProp, ...passThroughProps } = this.props;// 将 props 注入到被包装的组件中。// 通常为 state 的值或者实例方法。const injectedProps = someStateOrInstanceMethod;// 将 props 传递给被包裹的组件return (<wrappedcomponent injectedProps={injectedProps} {...passThroughProps} />);} -
约定:最大化可组合性
-
约定:包装显示名称以便轻松调试
注意事项
-
不要在 render 方法中使用 HOC
-
务必复制静态方法
-
Refs 不会被传递