React Native高階元件(HOC)模型理論與實踐
什麼是HOC?
HOC(全稱Higher-order component)是一種React的進階使用方法,主要還是為了便於元件的複用。HOC就是一個方法,獲取一個元件,返回一個更高階的元件。
什麼時候使用HOC?
在React開發過程中,發現有很多情況下,元件需要被"增強",比如說給元件新增或者修改一些特定的props,一些許可權的管理,或者一些其他的優化之類的。而如果這個功能是針對多個元件的,同時每一個元件都寫一套相同的程式碼,明顯顯得不是很明智,所以就可以考慮使用HOC。
栗子:react-redux的connect方法就是一個HOC,他獲取wrappedComponent,在connect中給wrappedComponent新增需要的props。
HOC的簡單實現
HOC不僅僅是一個方法,確切說應該是一個元件工廠,獲取低階元件,生成高階元件。
一個最簡單的HOC實現是這個樣子的:
function HOCFactory(WrappedComponent) {
return class HOC extends React.Component {
render(){
return <WrappedComponent {...this.props} />
}
}
}
HOC可以做什麼?
- 程式碼複用,程式碼模組化
- 增刪改props
- 渲染劫持
其實,除了程式碼複用和模組化,HOC做的其實就是劫持
增刪改props
可以通過對傳入的props進行修改,或者新增新的props來達到增刪改props的效果。
比如你想要給wrappedComponent增加一個props,可以這麼搞:
function control(wrappedComponent) { return class Control extends React.Component { render(){ let props = { ...this.props, message: "You are under control" }; return <wrappedComponent {...props} /> } } }
這樣,你就可以在你的元件中使用message這個props:
class MyComponent extends React.Component {
render(){
return <div>{this.props.message}</div>
}
}
export default control(MyComponent);
渲染劫持
這裡的渲染劫持並不是你能控制它渲染的細節,而是控制是否去渲染。由於細節屬於元件內部的render方法控制,所以你無法控制渲染細節。
比如,元件要在data沒有載入完的時候,現實loading...,就可以這麼寫:
function loading(wrappedComponent) {
return class Loading extends React.Component {
render(){
if(!this.props.data) {
return <div>loading...</div>
}
return <wrappedComponent {...props} />
}
}
}
這個樣子,在父級沒有傳入data的時候,這一塊兒就只會顯示loading...,不會顯示元件的具體內容
class MyComponent extends React.Component {
render(){
return <div>{this.props.data}</div>
}
}
export default control(MyComponent);
HOC有什麼用例?
React Redux
最經典的就是React Redux的connect方法(具體在connectAdvanced中實現)。
通過這個HOC方法,監聽redux store,然後把下級元件需要的state(通過mapStateToProps獲取)和action creator(通過mapDispatchToProps獲取)繫結到wrappedComponent的props上。
logger和debugger
這個是官網上的一個示例,可以用來監控父級元件傳入的props的改變:
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log(`WrappedComponent: ${WrappedComponent.displayName}, Current props: `, this.props);
console.log(`WrappedComponent: ${WrappedComponent.displayName}, Next props: `, nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
頁面許可權管理
可以通過HOC對元件進行包裹,當跳轉到當前頁面的時候,檢查使用者是否含有對應的許可權。如果有的話,渲染頁面。如果沒有的話,跳轉到其他頁面(比如無許可權頁面,或者登陸頁面)。
也可以給當前元件提供許可權的API,頁面內部也可以進行許可權的邏輯判斷。
本來準備把詳細程式碼當個栗子貼出來的,結果突然想到公司保密協議,所以。。。
使用HOC需要注意什麼?
儘量不要隨意修改下級元件需要的props
之所以這麼說,是因為修改父級傳給下級的props是有一定風險的,可能會造成下級元件發生錯誤。比如,原本需要一個name的props,但是在HOC中給刪掉了,那麼下級元件或許就無法正常渲染,甚至報錯。
Ref無法獲取你想要的ref
以前你在父元件中使用<component ref="component"/>
的時候,你可以直接通過this.refs.component
進行獲取。但是因為這裡的component經過HOC的封裝,已經是HOC裡面的那個component了,所以你無法獲取你想要的那個ref(wrappedComponent的ref)。
要解決這個問題,這裡有兩個方法:
a) 像React Redux的connect方法一樣,在裡面新增一個引數,比如withRef
,元件中檢查到這個flag了,就給下級元件新增一個ref,並通過getWrappedInstance方法獲取。
栗子:
function HOCFactory(wrappedComponent) {
return class HOC extends React.Component {
getWrappedInstance = ()=>{
if(this.props.widthRef) {
return this.wrappedInstance;
}
}
setWrappedInstance = (ref)=>{
this.wrappedInstance = ref;
}
render(){
let props = {
...this.props
};
if(this.props.withRef) {
props.ref = this.setWrappedInstance;
}
return <wrappedComponent {...props} />
}
}
}
export default HOCFactory(MyComponent);
這樣子你就可以在父元件中這樣獲取MyComponent的ref值了。
class ParentCompoent extends React.Component {
doSomethingWithMyComponent(){
let instance = this.refs.child.getWrappedInstance();
// ....
}
render(){
return <MyComponent ref="child" withRef />
}
}
b) 還有一種方法,在官網中有提到過:
父級通過傳遞一個方法,來獲取ref,具體看栗子:
先看父級元件:
class ParentCompoent extends React.Component {
getInstance = (ref)=>{
this.wrappedInstance = ref;
}
render(){
return <MyComponent getInstance={this.getInstance} />
}
}
HOC裡面把getInstance方法當作ref的方法傳入就好
function HOCFactory(wrappedComponent) {
return class HOC extends React.Component {
render(){
let props = {
...this.props
};
if(typeof this.props.getInstance === "function") {
props.ref = this.props.getInstance;
}
return <wrappedComponent {...props} />
}
}
}
export default HOCFactory(MyComponent);
感謝@wmzy的指出,在上面的兩個方法getInstance
和setWrappedInstance
,由於ES6 class
的寫法並不會自動繫結this
,所以需要用bind(this)
到兩個方法上,確保this
的正確性。或者使用箭頭函式來寫兩個方法,ES6的箭頭函式會自動繫結this
。
Component上面繫結的Static方法會丟失
比如,你原來在Component上面綁定了一些static方法MyComponent.staticMethod = o=>o
。但是由於經過HOC的包裹,父級元件拿到的已經不是原來的元件了,所以當然無法獲取到staticMethod方法了。
官網上的示例:
// 定義一個static方法
WrappedComponent.staticMethod = function() {/*...*/}
// 利用HOC包裹
const EnhancedComponent = enhance(WrappedComponent);
// 返回的方法無法獲取到staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
這裡有一個解決方法,就是hoist-non-react-statics
元件,這個元件會自動把所有繫結在物件上的非React方法都繫結到新的物件上:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
結束語
當你需要做React外掛的時候,HOC模型是一個很實用的模型。
希望這篇文章能幫你對HOC有一個大概的瞭解和啟發。
另外,這篇medium上的文章會給你更多的啟發,在這篇文章中,我這裡講的被分為Props Proxy HOC,還有另外一種Inheritance Inversion HOC,強烈推薦看一看。