高阶组件(HOC)是React开发中的特有名词,一个函数返回一个React组件,指的就是一个React组包裹着另外一个React组件。能够理解为一个生产React组件的工厂。javascript
有两种类型的HOC:html
WrappedComponent
的props进行操做。WrappedComponent
。一种最简单的Props Proxy实现java
function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return <WrappedComponent {...this.props}/> } } }
这里的HOC是一个方法,接受一个WrappedComponent
做为方法的参数,返回一个PP class,renderWrappedComponent
。使用的时候:react
const ListHOCInstance = ppHOC(List) <ListHOCInstance name='instance' type='hoc' />
这个例子中,咱们将本应该传给List
的props,传给了ppHoc返回的ListHOCInstance
(PP)上,在HOC内部咱们将PP的参数直接传给List
(WrappedComponent
)。这样的就至关于在List
外面加了一层代理,这个代理用于处理即将传给WrappedComponent
的props,这也是这种HOC为何叫Props Proxy。git
在pp中,咱们能够对WrappedComponent
进行如下操做:es6
WrappedComponent
好比添加新的props给WrappedComponent
:github
const isLogin = false; function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { const newProps = { isNew: true, login: isLogin } return <WrappedComponent {...this.props} {...newProps}/> } } }
WrappedComponent
组件新增了两个props:isNew和login。redux
function refsHOC(WrappedComponent) { return class RefsHOC extends React.Component { proc(wrappedComponentInstance) { wrappedComponentInstance.method() } render() { const props = Object.assign({}, this.props, {ref: this.proc.bind(this)}) return <WrappedComponent {...props}/> } } }
Ref 的回调函数会在 WrappedComponent
渲染时执行,你就能够获得WrappedComponent
的引用。这能够用来读取/添加实例的 props ,调用实例的方法。segmentfault
不过这里有个问题,若是WrappedComponent
是个无状态组件,则在proc中的wrappedComponentInstance
是null,由于无状态组件没有this,不支持ref。api
你能够经过传入 props 和回调函数把 state 提取出来,
function ppHOC(WrappedComponent) { return class PP extends React.Component { constructor(props) { super(props) this.state = { name: '' } this.onNameChange = this.onNameChange.bind(this) } onNameChange(event) { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {...this.props} {...newProps}/> } } }
使用的时候:
class Test extends React.Component { render () { return ( <input name="name" {...this.props.name}/> ); } } export default ppHOC(Test);
这样的话,就能够实现将input转化成受控组件。
这个比较好理解,就是将WrappedComponent组件外面包一层须要的嵌套结构
function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return ( <div style={{display: 'block'}}> <WrappedComponent {...this.props}/> </div> ) } } }
先看一个反向继承(ii)的?:
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { return super.render() } } }
上面例子能够看出来Enhancer继承了WrappedComponent,可是Enhancer能够经过super关键字获取到父类原型对象上的全部方法(父类实例上的属性或方法则没法获取)。在这种方式中,它们的关系看上去被反转(inverse)了。
咱们可使用Inheritance Inversion实现如下功能:
之因此被称为渲染劫持是由于 HOC 控制着 WrappedComponent 的渲染输出,能够用它作各类各样的事。经过渲染劫持咱们能够实现:
demo1:条件渲染
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { if (!this.props.loading) { return super.render() } else { return <div>loading</div> } } } }
demo2:修改渲染
function iiHOC(WrappedComponent) { return class Enhancer extends WrappedComponent { render() { const elementsTree = super.render() let newProps = {}; if (elementsTree && elementsTree.type === 'input') { newProps = {value: 'may the force be with you'} } const props = Object.assign({}, elementsTree.props, newProps) const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children) return newElementsTree } } }
在这个例子中,若是 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”。
你能够在这里作各类各样的事,你能够遍历整个元素树,而后修改元素树中任何元素的 props。
HOC 能够读取、编辑和删除 WrappedComponent 实例的 state,若是你须要,你也能够给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,致使你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一块儿。
export function IIHOCDEBUGGER(WrappedComponent) { return class II extends WrappedComponent { render() { return ( <div> <h2>HOC Debugger Component</h2> <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre> <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre> {super.render()} </div> ) } } }
使用高阶组件的时候,也会遇到一些问题:
当使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的全部静态方法。
可使用hoist-non-react-statics来帮你自动处理,它会自动拷贝全部非React的静态方法:
import hoistNonReactStatic from 'hoist-non-react-statics'; export default (title = '默认标题') => (WrappedComponent) => { class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>{title}</legend> <WrappedComponent {...this.props} /> </fieldset> ); } } // 拷贝静态方法 hoistNonReactStatic(HOC, WrappedComponent); return HOC; };
通常来讲,高阶组件能够传递全部的props属性给包裹的组件,可是不能传递 refs
引用。由于并非像 key
同样,refs
是一个伪属性,React
对它进行了特殊处理。
若是你向一个由高级组件建立的组件的元素添加 ref
应用,那么 ref
指向的是最外层容器组件实例的,而不是包裹组件。
但有的时候,咱们不可避免要使用 refs
,官方给出的解决方案是:
传递一个ref回调函数属性,也就是给ref应用一个不一样的名字
const Hello = createReactClass({ componentDidMount: function() { var component = this.hello; // ...do something with component }, render() { return <div ref={(c) => { this.hello = c; }}>Hello, world.</div>; } });
这里要注意的是:
JSX
和React.createClass
建立的都是元素。反向继承不能保证完整的子组件树被解析的意思的解析的元素树中包含了组件(函数类型或者Class类型),就不能再操做组件的子组件了,这就是所谓的不能彻底解析。详细介绍可参考官方博客。
官方文档中也对使用高阶组件有一些约定
高阶组件给组件添加新特性。他们不该该大幅修改原组件的接口(译者注:应该就是props属性)。预期,从高阶组件返回的组件应该与原包裹的组件具备相似的接口。
高阶组件应该传递与它要实现的功能点无关的props属性。大多数高阶组件都包含一个以下的render函数:
render() { // 过滤掉与高阶函数功能相关的props属性, // 再也不传递 const { extraProp, ...passThroughProps } = this.props; // 向包裹组件注入props属性,通常都是高阶组件的state状态 // 或实例方法 const injectedProp = someStateOrInstanceMethod; // 向包裹组件传递props属性 return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); }
并非全部的高阶组件看起来都是同样的。有时,它们仅仅接收一个参数,即包裹组件:
const NavbarWithRouter = withRouter(Navbar);
通常而言,高阶组件会接收额外的参数。在下面这个来自Relay的示例中,可配置对象用于指定组件的数据依赖关系:
const CommentWithRelay = Relay.createContainer(Comment, config);
大部分常见高阶组件的函数签名以下所示:
// React Redux's `connect` const ConnectedComment = connect(commentSelector, commentActions)(Comment);
这是什么?! 若是你把它剥开,你就很容易看明白究竟是怎么回事了。
// connect是一个返回函数的函数(译者注:就是个高阶函数) const enhance = connect(commentListSelector, commentListActions); // 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store // 关联起来的新组件 const ConnectedComment = enhance(CommentList);
换句话说,connect
是一个返回高阶组件的高阶函数!
这种形式有点让人迷惑,有点多余,可是它有一个有用的属性。那就是,相似 connect
函数返回的单参数的高阶组件有着这样的签名格式, Component => Component
.输入和输出类型相同的函数是很容易组合在一块儿。
// 不要这样作…… const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent)) // ……你可使用一个功能组合工具 // compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的 const enhance = compose( // 这些都是单参数的高阶组件 connect(commentSelector), withRouter ) const EnhancedComponent = enhance(WrappedComponent)
(connect
函数产生的高阶组件和其它加强型高阶组件具备一样的被用做装饰器的能力。)
包括lodash(好比说lodash.flowRight
), Redux
和 Ramda
在内的许多第三方库都提供了相似compose
功能的函数。
高价组件建立的容器组件在React Developer Tools
中的表现和其它的普通组件是同样的。为了便于调试,能够选择一个好的名字,确保可以识别出它是由高阶组件建立的新组件仍是普通的组件。
最经常使用的技术就是将包裹组件的名字包装在显示名字中。因此,若是你的高阶组件名字是 withSubscription
,且包裹组件的显示名字是 CommentList
,那么就是用 withSubscription(CommentList)
这样的显示名字:
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription; } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }