Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.javascript
经过函数向现有组件类添加逻辑,就是高阶组件。html
让咱们先来看一个多是史上最无聊的高阶组件:java
function noId() { return function(Comp) { return class NoID extends Component { render() { const {id, ...others} = this.props; return ( <Comp {...others}/> ) } } } } const WithoutID = noId()(Comp);
这个例子向咱们展现了高阶组件的工做方式:经过函数和闭包,改变已有组件的行为——这里是忽略id
属性——而彻底不须要修改任何代码。react
之因此称之为高阶
,是由于在React中,这种嵌套关系会反映到组件树上,层层嵌套就好像高阶函数的function in function同样,如图:git
从图上也能够看出,组件树虽然嵌套了多层,可是实际渲染的DOM结构并无改变。
若是你对这点有疑问,不妨本身写写例子试下,加深对React的理解。如今能够先记下结论:咱们能够放心的使用多层高阶组件,甚至重复地调用,而没必要担忧影响输出的DOM结构。es6
借助函数的逻辑表现力,高阶组件的用途几乎是无穷无尽的:github
有的时候你须要替换一些已有组件,而新组件接收的参数和原组件并不彻底一致。编程
你能够修改全部使用旧组件的代码来保证传入正确的参数——考虑改行吧若是你真这么想redux
也能够把新组件作一层封装:segmentfault
class ListAdapter extends Component { mapProps(props) { return {/* new props */} } render() { return <NewList {...mapProps(this.props)} /> } }
若是有十个组件须要适配呢?若是你不想照着上面写十遍,或许高阶组件能够给你答案
function mapProps(mapFn) { return function(Comp) { return class extends Component { render() { return <Comp {...mapFn(this.props)}/> } } } } const ListAdapter = mapProps(mapPropsForNewList)(NewList);
借助高阶组件,关注点被分离得更加干净:只须要关注真正重要的部分——属性的mapping。
这个例子有些价值,却仍然不够打动人,若是你也这么想,请往下看:
纯组件易写易测,越多越好,这是常识。然而在实际项目中,每每有许多的状态和反作用须要处理,最多见的状况就是异步了。
假设咱们须要异步加载一个用户列表,一般的代码多是这样的:
class UserList extends Component { constructor(props) { super(); this.state = { list: [] } } componentDidMount() { loadUsers() .then(data=> this.setState({list: data.userList}) ) } render() { return ( <List list={this.state.list} /> ) } /* other bussiness logics */ }
实际状况中,以上代码每每还会和其它一些业务函数混杂在一块儿——咱们建立了一个业务与反作用混杂的、有状态的组件。
若是再来一个书单列表呢?再写一个BookList而后把loadUsers改为loadBooks ?
不只代码重复,大量有状态和反作用的组件,也使得应用更加难以测试。
也许你会考虑使用Flux。它确实能让你的代码更清晰,可是在有些场景下使用Flux就像大炮打蚊子。好比一个异步的下拉选择框,若是要考虑复用的话,传统的Flux/Reflux几乎没法优雅的处理,Redux稍好一些,但仍然很难作优雅。关于flux/redux的缺点不深刻,有兴趣的能够参考Cycle.js做者的文章
回到问题的本源:其实咱们只想要一个能复用的异步下拉列表而已啊!
高阶函数试试?
import React, { Component } from 'react'; const DEFAULT_OPTIONS = { mapStateToProps: undefined, mapLoadingToProps: loading => ({ loading }), mapDataToProps: data => ({ data }), mapErrorToProps: error => ({ error }), }; export function connectPromise(options) { return (Comp) => { const finalOptions = { ...DEFAULT_OPTIONS, ...options, }; const { promiseLoader, mapLoadingToProps, mapStateToProps, mapDataToProps, mapErrorToProps, } = finalOptions; class AsyncComponent extends Component { constructor(props) { super(props); this.state = { loading: true, data: undefined, error: undefined, }; } componentDidMount() { promiseLoader(this.props) .then( data => this.setState({ data, loading: false }), error => this.setState({ error, loading: false }), ); } render() { const { data, error, loading } = this.state; const dataProps = data ? mapDataToProps(data) : undefined; const errorProps = error ? mapErrorToProps(error) : undefined; return ( <Comp {...mapLoadingToProps(loading)} {...dataProps} {...errorProps} {...this.props} /> ); } } return AsyncComponent; }; } const UserList = connectPromise({ promiseLoader: loadUsers, mapDataToProps: result=> ({list: result.userList}) })(List); //List can be a pure component const BookList = connectPromise({ promiseLoader: loadBooks, mapDataToProps: result=> ({list: result.bookList}) })(List);
不只大大减小了重复代码,还把散落各处的异步逻辑装进了能够单独管理和测试的笼子,在业务场景中,只须要纯组件 + 配置
就能实现相同的功能——而不管是纯组件
仍是配置
,都是对单元测试友好的,至少比异步组件友好多了。
高阶组件的另外一个亮点,就是对函数式编程的友好。你可能已经注意到,目前我写的全部高阶函数,都是形如:
config => { return Component=> { return HighOrderCompoent } }
表示为config=> Component=> Component
。
写成嵌套的函数是为了手动curry化,而参数的顺序(为何不是Component=> config=> Component
),则是为了组合方便。关于curry与compose的使用,能够移步个人另外一篇blog
举个栗子,前面讲了适配器和异步,咱们能够很快就组合出二者的结合体:使用NewList的异步用户列表
UserList = compose( connectPromise({ promiseLoader: loadUsers, mapResultToProps: result=> ({list: result.userList}) }), mapProps(mapPropsForNewList) )(NewList);
在团队内部分享里,个人总结是三个词 Easy, Light-weight & Composable.
其实高阶组件并非什么新东西,本质上就是Decorator
模式在React的一种实现,但在至关一段时间内,这个优秀的模式都被人忽略。在我看来,大部分使用mixin
和class extends
的地方,高阶组件都是更好的方案——毕竟组合优于继承,而mixin——我的以为没资格参与讨论。
使用高阶组件还有两个好处:
适用范围广,它不须要es6或者其它须要编译的特性,有函数的地方,就有HOC。
Debug友好,它可以被React组件树显示,因此能够很清楚地知道有多少层,每层作了什么。相比之下不管是mixin仍是继承,都显得很是隐晦。
值得庆幸的是,社区也明显注意到了高阶组件的价值,不管是你们很是熟悉的react-redux 的connect
函数,仍是redux-form,高阶组件的应用开始随处可见。
下次当你想写mixin
或class extends
的时候,不妨也考虑下高阶组件。