高阶组件HOC
2021 react高阶组件 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 '..';
// 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 不会被传递