最后更新时间:2019/05/15
以下内容来自:
- React 官网文档
- Robin Wieruch 博客
- 《The Road to learn React Your journey to master plain yet pragmatic React.js》
自身理解:(
1 HOC 基础概念
1.1 定义
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature
They take any input - most of the time a component, but also optional arguments - and return a component as output. The returned component is an enhanced version of the input component and can be used in your JSX
const EnhancedComponent = higherOrderComponent(WrappedComponent);
1.2 理解
- 高阶组件只是 React 建议的一种机制、模式,并非一个特殊的 API。
- HOC 的目的在于通过将不同的 Component 中相同的逻辑提取出来,在一个 function 实现这些通用逻辑,之后接受 Component 输入,“注入”通用逻辑,实现对 component 的增强,减少代码冗余,提高组件的复用性。这种通用逻辑的注入可以是向 Component 注入新的 prop,可以是对 Component 的 prop 进行某种检查,进行条件渲染等。
- HOC 的返回可以是一个 class 组件,function 组件或者另外的 HOC
- 传入的 Component 作为
return
新的增强的组件的相对独立的一部分,因此 不要在高阶组件中直接修改传入组件(方法等)
2 实例
2.1 对 Component 注入新的 prop
2.1.1 思考与使用过程
现在存在两个组件,CommentList
与BlogPost
,他们都从一个外部数据DataSource
中获取数据进行展示。
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments(),
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
comments: DataSource.getComments(),
});
}
render() {
return (
{this.state.comments.map(comment => (
))}
);
}
}
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id),
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id),
});
}
render() {
return ;
}
}
两个组件的区别:
- 从DataSource中获取数据的方法不同,一个是
getComments
,一个是getBlogPost
- 展示数据的
render
函数不同。
相同点:
- 在组件挂载时 subscribe DataSource,当
DataSource
发生改变后,调用handleChange
重新渲染;同时卸载时移除 listener - 都从DataSource中获取数据
可以看到这两个组件存在相同的逻辑,即从DataSource中获取数据,进行渲染。当中存在冗余的代码,如果再写第三个组件,如IssueList
,那么这个逻辑还要重复一个。
因此我们可以采用以下的高阶组件提取通用逻辑:
(1)定义高阶组件
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props),
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props),
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return ;
}
};
}
此 HOC 接受WrappedComponent
,以及selectData
两个参数,前者是需要增强的组件,后者是用来从DataSource中获取数据的 function。
注意到 HOC 返回的是一个增强的、新的 class react 组件,具有以下几个特征:
- local
state
中保存了通过 HOC 参数selectData
拿到的数据 -
render
函数返回的还是传入的WrappedComponent
组件的实例,并且传入了一个新的data
属性。 - 需要注意,
{...this.props}
,保证了高阶组件实例生成时定传入的props
都能够传入WrappedComponent
组件。
(2)重新定义原组件
重新实现之前的CommentList
与BlogPost
组件,此时在它们的render
函数中直接使用this.props.data
来进行渲染,不需要再与DataSource
进行交互。
class CommentList extends React.Component {
render() {
const { data, ...res } = this.props;
return (
{data.map(comment => (
))}
);
}
}
class CommentList extends React.Component {
render() {
const { data, ...res } = this.props;
return ;
}
}
(3)定义增强组件
const CommentListWithSubscription = withSubscription(CommentList, DataSource =>
DataSource.getComments(),
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id),
);
此时的CommentListWithSubscription
与BlogPostWithSubscription
是高阶组件withSubscription
返回的新增强的 class 组件。
(4) 使用新的增强组件
class App extends Component{
...
render() {
...
return (
)
}
}
注意其中的disableed
属性会一层层的传递给Comment
组件,传递过程如下:
- 首先是传入
withSubscription
返回的EnhancedComponent
组件 render 函数中的props
- 通过 return 语句中
{...this.props}
被传递给
组件 - 此时
WrappedComponent
是CommentList
,在它的 render 函数可通过{...res}
传递给
2.1.2 总结
可以看到这种方式的 HOC 没有直接改变传入的 Component,而是传入新的prop
,因此,在 Component 的render
函数中可以使用新的prop
进行渲染或其它操作。以上例子在React docs进一步了解。
2.2 条件渲染
现在存在一个ToDoList
组件
function TodoList({ todos, isLoadingTodos }) {
if (isLoadingTodos) {
return (
Loading todos ...
);
}
if (!todos) {
return null;
}
if (!todos.length) {
return (
You have no Todos.
);
}
return (
{todos.map(todo => (
))}
);
}
可以看到有很多关于 todos 的条件渲染,我们可以尝试把这种条件渲染的逻辑提取出来,形成下面的情况:
const withLoadingIndicator = Component => ({ isLoadingTodos, ...others }) =>
isLoadingTodos ? (
Loading todos ...
) : (
); // (1)
const withTodosNull = Component => props =>
!props.todos ? null : ; // (2)
const withTodosEmpty = Component => props =>
!props.todos.length ? (
You have no Todos.
) : (
); // (3)
重新定义ToDoList
组件:
const TodoList = ({ todos }) => (
{todos.map(todo => (
))}
);
之后利用这三个新的 HOC,定义新的增强组件:
const TodoListWithConditionalRendering = withLoadingIndicator(
withTodosNull(withTodosEmpty(TodoList)),
);
// 可以写成以下的形式
// const TodoListOne = withTodosEmpty(TodoList);
// const TodoListTwo = withTodosNull(TodoListOne);
// const TodoListThree = withLoadingIndicator(TodoListTwo);
现在,生成增强组件的实例:
...
...
isLoadingTodos
属性通过 (1)(2)(3)
层层传递给TodoList
组件。
更多的了解可以参看A gentle Introduction to React's Higher Order Components
2.3 第三方库 HOC 例子
让我们来看实际的例子:
// antd Form组件的使用 参见https://ant.design/components/form-cn/#Form.create(options)
import { Form } from 'antd';
class CustomizedForm extends React.Component {}
export default (CustomizedForm = Form.create({})(CustomizedForm));
代码中的Form.create()
方法接受一个option
参数,该参数的部分属性如下表:
参数 | 说明 |
---|---|
name |
设置表单域内字段id 的前缀 |
onValuesChange |
任一表单域的值发生改变时的回调 |
一个使用的例子如下:
const CustomizedForm = Form.create({
name: 'global_state',
onFieldsChange(props, changedFields) {
props.onChange(changedFields);
},
mapPropsToFields(props) {
return {
username: Form.createFormField({
...props.username,
value: props.username.value,
}),
};
},
onValuesChange(_, values) {
console.log(values);
},
})(CustomComponent);
Form.create()
方法返回的还是一个 HOC,这个 HOC 单独接受一个组件输入,返回增强组件,即上面的Form.create({})(CustomizedForm)
。
此时在CustomizedForm
组件中就可以使用被高阶组件注入的属性form
。例如:
// CustomizedForm
render() {
const {
getFieldDecorator, getFieldsError, getFieldError, isFieldTouched,
} = this.props.form;
// Only show error after a field is touched.
const userNameError = isFieldTouched('userName') && getFieldError('userName');
return (
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
} placeholder="Username" />
)}
);
}
2.4 使用注意事项
定义的 HOC 可以采用
with
开头-
不要在
render
中使用 HOC,而是在render
之外就使用 HOC 定义好新的增强组件,在 render 函数中直接使用 HOC 返回的增强组件。原因有以下两点:- 效率:每次
render
执行时,都使用 HOC 生成新的增强组件,一方面效率较低,另一方面virtual DOM
和real DOM
比较时,新的增强组件与旧的组件不会认为是相同的。 - 增强组件的状态丢失:每次
render
执行会卸载之前的增强组件,导致其中的state
丢失
- 效率:每次
-
静态方法需要特别对待:如果在原始组件中定义了静态方法,之后使用 HOC 返回的增强组件是没有该静态方法的。如:
// Define a static method WrappedComponent.staticMethod = function() { /*...*/ }; // Now apply a HOC const EnhancedComponent = enhance(WrappedComponent); // The enhanced component has no static method typeof EnhancedComponent.staticMethod === 'undefined'; // true
要解决这个问题,需要拷贝该静态方法:
function enhance(WrappedComponent) { class Enhance extends React.Component { /*...*/ } // Must know exactly which method(s) to copy :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
ref
属性无法传递,原因在于ref
不是和其它普通 prop 一起存在props
中的,它会被 React 特殊处理,ref
只会指向增强组件,而不是被包裹的原始 Component。解决这个问题在于使用React.forwardRef
, Learn more about it in the forwarding refs section.