最后更新时间: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新的增强的组件的相对独立的一部分,因此 <span style=“text-color: red”>不要在高阶组件中直接修改传入组件(方法等)</span>
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 (
<div>
{this.state.comments.map(comment => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
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 <TextBlock text={this.state.blogPost} />;
}
}
两个组件的区别:
- 从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 <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
此 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 (
<div>
{data.map(comment => (
<Comment comment={comment} key={comment.id} {...reas} />
))}
</div>
);
}
}
class CommentList extends React.Component {
render() {
const { data, ...res } = this.props;
return <TextBlock text={data} {...res} />;
}
}
(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 (
<div>
<CommentListWithSubscription disabled/>
<BlogPostWithSubscription />
</div>
)
}
}
注意其中的disableed属性会一层层的传递给Comment组件,传递过程如下:
- 首先是传入
withSubscription返回的EnhancedComponent组件 render 函数中的props - 通过 return 语句中
{...this.props}被传递给<WrappedComponent />组件 - 此时
WrappedComponent是CommentList,在它的 render 函数可通过{...res}传递给<Component />
2.1.2 总结
可以看到这种方式的 HOC 没有直接改变传入的 Component,而是传入新的prop,因此,在 Component 的render函数中可以使用新的prop进行渲染或其它操作。以上例子在React docs进一步了解。
2.2 条件渲染
现在存在一个ToDoList组件
function TodoList({ todos, isLoadingTodos }) {
if (isLoadingTodos) {
return (
<div>
<p>Loading todos ...</p>
</div>
);
}
if (!todos) {
return null;
}
if (!todos.length) {
return (
<div>
<p>You have no Todos.</p>
</div>
);
}
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
可以看到有很多关于 todos 的条件渲染,我们可以尝试把这种条件渲染的逻辑提取出来,形成下面的情况:
const withLoadingIndicator = Component => ({ isLoadingTodos, ...others }) =>
isLoadingTodos ? (
<div>
<p>Loading todos ...</p>
</div>
) : (
<Component {...others} />
); // (1)
const withTodosNull = Component => props =>
!props.todos ? null : <Component {...props} />; // (2)
const withTodosEmpty = Component => props =>
!props.todos.length ? (
<div>
<p>You have no Todos.</p>
</div>
) : (
<Component {...props} />
); // (3)
重新定义ToDoList组件:
const TodoList = ({ todos }) => (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
之后利用这三个新的 HOC,定义新的增强组件:
const TodoListWithConditionalRendering = withLoadingIndicator(
withTodosNull(withTodosEmpty(TodoList)),
);
// 可以写成以下的形式
// const TodoListOne = withTodosEmpty(TodoList);
// const TodoListTwo = withTodosNull(TodoListOne);
// const TodoListThree = withLoadingIndicator(TodoListTwo);
现在,生成增强组件的实例:
...
<TodoListWithConditionalRendering isLoadingTodos={true} />
...
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 (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</Form.Item>
</Form>
);
}
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.