[react] Higher Order Component(HOC)
Higher Order Component 指的是在 React 中能夠幫助我們重複使用程式碼的 React Component。具體來說 Higher Order Component 是一個 function,而這個 function 可以把 Component 當作參數傳入,並且回傳一個「增強版」的 Component。
- 被當作參數放入的 Component 稱作 Wrapped Component (ChildComponent),因為它是被 HOC 包住的。
- Higher-Order Component 又稱作 Enhanced Component 或 Composed Component,但它其實是 Function。
最常見的就是 react-redux
中的 connect()
這個函式。
撰寫 Higher Order Component
撰寫 Higher Order Component(HOC)的流程如下:
- 將你想要重複使用的程式碼或邏輯撰寫成 React Component
- 建立 HOC 檔案,並撰寫草稿
- 將想要重複使用的程式碼或邏輯搬移到 HOC 檔案中
- 將 props/config/behavior 傳遞到子元件(child component)中
HOC boilerplate
// '@/components/HocComponent'
import React from 'react';
const higherOrderComponent = (ChildComponent) => {
class ComposedComponent extends React.Component {
render() {
// 記得要用 ...this.props 把所有原本的 props 內容帶回到 ChildComponent 中
return <ChildComponent {...this.props} />;
}
}
return ComposedComponent;
};
export default higherOrderComponent;
❗ 記得要在
return <ChildComponent>
中使用{...this.props}
把原有的 props 代到 ChildComponent 中。
使用 HOC 的方式是在要套用此 HOC 的元件載入,並在最後 export
的時候呼叫它:
// '@/components/ChildComponent'
import HocComponent from '@/component/HocComponent';
class WrappedComponent {
// ...
render() { ... }
}
export default HocComponent(WrappedComponent);
範例程式碼
/* This is a HOC */
import React from 'react';
import { connect } from 'react-redux';
export default (ChildComponent) => {
class ComposedComponent extends React.Component {
// Our component just got rendered
componentDidMount() {
this.verifyAuth();
}
// Our component just got updated
componentDidUpdate() {
this.verifyAuth();
}
verifyAuth() {
if (this.props.auth) {
return;
}
this.props.history.push('/');
}
render() {
// 記得要用 ...this.props 把所有原本的 props 內容帶回到 ChildComponent 中
return <ChildComponent {...this.props} />;
}
}
const mapStateToProps = (state) => ({
auth: state.auth,
});
return connect(mapStateToProps)(ComposedComponent);
};
慣例與注意事項
慣例:透過 Wrapped Component 將無關的 Props 傳入
HOCs 只是為元件添加功能,所以要把被當作參數傳入的 Component 提供相同的 props。因此要把這個 HOC 用不到的 props 全部在還回到原本的 Component 中:
render() {
// 將在這個 HOC 需要用到的 props 取出,其於的 props 傳回原本的 Wrapped Component 內
const { extraProp, ...passThroughProps } = this.props;
// 灌入新的 props 進去原本的 Wrapped Component 中
const injectedProp = someStateOrInstanceMethod;
return (
// 把這些 Props 都放回當作參數傳入的 Wrapped Component
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
慣例:最大化相容性(composability)
撰寫 HOC 時有幾種不同做法,但建議上還是讓 HOC 只帶一個參數(即,WrappedComponent
):
// enhancedComponent(WrappedComponent)
const NavbarWithRouter = withRouter(Navbar);
// enhancedComponent(config)(WrappedComponent)
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
// NOT RECOMMENDED: enhancedComponent(WrappedComponent, config)
const CommentWithRelay = Relay.createContainer(Comment, config);
因為在許多第三方提供的套件中,都有提供 compose
utility function,它可以把多個 HOC 連接起來,但前提是這些 HOC 都只能接受 WrappedComponent
當作它的單一參數:
const enhance = compose(
// 這些都是只接受單一參數的 HOC
withRouter,
connect(commentSelector),
);
慣例:把 displayName 包在裡面方便除錯
舉例來說假設你的 HOC 稱作 withSubscription
,而 wrapped component 稱作 CommentList
,那麼用 WithSubscription(CommentList)
來當作顯示的名稱:
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {
/* ... */
}
// 使用 displayName 屬性定義名稱,方便使用 devtool 除錯
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
注意: 不要變動原本的 Component,使用 Composition
千萬不要在 HOC 裡去改變(mutate)傳入元件的 prototype。這麼做的話會使得 HOC 和這個傳入的 Component 產生耦合的情況,也就是 HOC 沒辦法和這個傳入的元件拆開來使用,此外若你同時使用了其他的 HOC,這個 HOC 的 prototype 也會被覆蓋掉;再者,這樣做的話你的 HOC 將不能傳入 function components,因為 function component 並沒有生命週期的方法在內。
不要這麼做 👎
// DON'T DO THIS
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps = function (nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
};
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
要這麼做 👍
使用 composition:
// DO THIS
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
};
}
注意:千萬不要在 render 方法中使用 HOC
// Don’t Use HOCs Inside the render Method
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
注意:refs 不會被當作 props 傳入
參考
- Advanced React and Redux @ Udemy
- Higher-Order Components @ React 官網
- 當初要是看了這篇,React 高階元件早會了
- 深入理解 React 高階元件