本期精讀文章是:React Higher Order Components in depth
1 引言
高階組件( higher-order component ,HOC )是 React 中復用組件邏輯的一種進階技巧。它本身并不是 React 的 API,而是一種 React 組件的設計理念,眾多的 React 庫已經證明了它的價值,例如耳熟能詳的 react-redux。
高階組件的概念其實并不難,我們能通過類比高階函數迅速掌握。高階函數是把函數作為參數傳入到函數中并返回一個新的函數。這里我們把函數替換為組件,就是高階組件了。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
當然了解高階組件的概念只是萬里長征第一步,精讀文章在闡述其概念與實現外,也強調了其重要性與局限性,以及與其他方案的比較,讓我們一起來領略吧。
2 內容概要
高階組件常見有兩種實現方式,一種是 Props Proxy,它能夠對 WrappedComponent 的 props 進行操作,提取 WrappedComponent state 以及使用其他元素來包裹 WrappedComponent。Props Proxy 作為一層代理,具有隔離的作用,因此傳入 WrappedComponent 的 ref 將無法訪問到其本身,需要在 Props Proxy 內完成中轉,具體可參考以下代碼,react-redux 也是這樣實現的。
此外各個 Props Proxy 的默認名稱是相同的,需要根據 WrappedComponent 來進行不同命名。
function ppHOC(WrappedComponent) {return class PP extends React.Component {// 實現 HOC 不同的命名static displayName = `HOC(${WrappedComponent.displayName})`;getWrappedInstance() {return this.wrappedInstance;}// 實現 ref 的訪問setWrappedInstance(ref) {this.wrappedInstance = ref;}render() {return <WrappedComponent {...this.props,ref: this.setWrappedInstance.bind(this),} />}}
}@ppHOC
class Example extends React.Component {static displayName = 'Example';handleClick() { ... }...
}class App extends React.Component {handleClick() {this.refs.example.getWrappedInstance().handleClick();}render() {return (<div><button onClick={this.handleClick.bind(this)}>按鈕</button><Example ref="example" /></div> );}
}
另一種是 Inheritance Inversion,HOC 類繼承了 WrappedComponent,意味著可以訪問到 WrappedComponent 的 state、props、生命周期和 render 等方法。如果在 HOC 中定義了與 WrappedComponent 同名方法,將會發生覆蓋,就必須手動通過 super 進行調用了。通過完全操作 WrappedComponent 的 render 方法返回的元素樹,可以真正實現渲染劫持。這種方案依然是繼承的思想,對于 WrappedComponent 也有較強的侵入性,因此并不常見。
function ppHOC(WrappedComponent) {return class ExampleEnhance extends WrappedComponent {...componentDidMount() {super.componentDidMount();}componentWillUnmount() {super.componentWillUnmount();}render() {...return super.render();}}
}
3 精讀
本次提出獨到觀點的同學有:
@monkingxue @alcat2008 @淡蒼 @camsong,精讀由此歸納。
HOC 的適用范圍
對比 HOC 范式 compose(render)(state)
與父組件(Parent Component)的范式 render(render(state))
,如果完全利用 HOC 來實現 React 的 implement,將操作與 view 分離,也未嘗不可,但卻不優雅。HOC 本質上是統一功能抽象,強調邏輯與 UI 分離。但在實際開發中,前端無法逃離 DOM ,而邏輯與 DOM 的相關性主要呈現 3 種關聯形式:
- 與 DOM 相關,建議使用父組件,類似于原生 HTML 編寫
- 與 DOM 不相關,如校驗、權限、請求發送、數據轉換這類,通過數據變化間接控制 DOM,可以使用 HOC 抽象
- 交叉的部分,DOM 相關,但可以做到完全內聚,即這些 DOM 不會和外部有關聯,均可
DOM 的渲染適合使用父組件,這是 React JSX 原生支持的方式,清晰易懂。最好是能封裝成木偶組件(Dumb Component)。HOC 適合做 DOM 不相關又是多個組件共性的操作。如 Form 中,validator 校驗操作就是純數據操作的,放到了 HOC 中。但 validator 信息沒有放到 HOC 中。但如果能把 Error 信息展示這些邏輯能夠完全隔離,也可以放到 HOC 中(可結合下一小節 Form 具體實踐詳細了解)。
數據請求是另一類 DOM 不相關的場景,react-refetch 的實現就是使用了 HOC,做到了高效和優雅:
connect(props => ({usersFetch: `/users?status=${props.status}&page=${props.page}`,userStatsFetch: { url: `/users/stats`, force: true }
}))(UsersList)
HOC 的具體實踐
HOC 在真實場景下的運行非常多,之前筆者在 基于 Decorator 的組件擴展實踐 一文中也提過使用高階組件將更細粒度的組件組合成 Selector 與 Search。結合精讀文章,這次讓我們通過 Form 組件的抽象來表現 HOC 具有的良好擴展機制。
Form 中會包含各種不同的組件,常見的有 Input、Selector、Checkbox 等等,也會有根據業務需求加入的自定義組件。Form 靈活多變,從功能上看,表單校驗可能為單組件值校驗,也可能為全表單值校驗,可能為常規檢驗,比如:非空、輸入限制,也可能需要與服務端配合,甚至需要根據業務特點進行定制。從 UI 上看,檢驗結果顯示的位置,可能在組件下方,也可能是在組件右側。
直接裸寫 Form,無疑是機械而又重復的。將 Form 中組件的 value 經過 validator,把 value,validator 產生的 error 信息儲存到 state 或 redux store 中,然后在 view 層完成顯示。這條路大家都是相同的,可以進行復用,只是我們面對的是不同的組件,不同的 validator,不同的 view 而已。對于 Form 而言,既要滿足通用,又要滿足部分個性化的需求,以往單純的配置化只會讓使用愈加繁瑣,我們所需要抽象的是 Form 功能而非 UI,因此通過 HOC 針對 Form 的功能進行提取就成為了必然。
至于 HOC 在 Form 上的具體實現,首先將表單中的組件(Input、Selector…)與相應 validator 與組件值回調函數名(trigger)傳入 Decorator,將 validator 與 trigger 相綁定。Decorator 完成了各種不同組件與 Form 內置 Store 間 value 的傳遞、校驗功能的抽象,即精讀文章中提到 Props Proxy 方式的其中兩種作用:提取 state 與 操作 props
function formFactoryFactory({validator,trigger = 'onChange',...
}) {return FormFactory(WrappedComponent) {return class Decorator extends React.Component {getBind(trigger, validator) {...}render() {const newProps = {...this.props,[trigger]: this.getBind(trigger, validator),...}return <WrappedComponent {...newProps} />}}}
}// 調用
formFactoryFactory({validator: (value) => {return value !== '';}
})(<Input placeholder="請輸入..." />)
當然為了考慮個性化需求,Form Store 也向外暴露很多 API,可以直接獲取和修改 value、error 的值。現在我們需要對一個表單的所有值提交到后端進行校驗,根據后端返回,分別列出各項的校驗錯誤信息,就需要借助相應項的 setError 去完成了。
這里主要參考了 rc-form 的實現方式,有興趣的讀者可以閱讀其源碼。
import { createForm } from 'rc-form';class Form extends React.Component {submit = () => {this.props.form.validateFields((error, value) => {console.log(error, value);});}render() {const { getFieldError, getFieldDecorator } = this.props.form;const errors = getFieldError('required');return (<div>{getFieldDecorator('required', {rules: [{ required: true }],})(<Input />)}{errors ? errors.join(',') : null}<button onClick={this.submit}>submit</button></div>);}
}export createForm()(Form);
4 總結
React 始終強調組合優于繼承的理念,期望通過復用小組件來構建大組件使得開發變得簡單而又高效,與傳統面向對象思想是截然不同的。高階函數(HOC)的出現替代了原有 Mixin 侵入式的方案,對比隱式的 Mixin 或是繼承,HOC 能夠在 Devtools 中顯示出來,滿足抽象之余,也方便了開發與測試。當然,不可過度抽象是我們始終要秉持的原則。希望讀者通過本次閱讀與討論,能結合自己具體的業務開發場景,獲得一些啟發。