高階組件(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'; }