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 中顯示出來,滿足抽象之餘,也友善了開發與測試。當然,不可過度抽象是我們始終要秉持的原則。希望讀者通過本次閱讀與讨論,能結合自己具體的業務開發場景,獲得一些啟發。