跳至主要内容

[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 ComponentComposed Component,但它其實是 Function。

最常見的就是 react-redux 中的 connect() 這個函式。

撰寫 Higher Order Component

撰寫 Higher Order Component(HOC)的流程如下:

  1. 將你想要重複使用的程式碼或邏輯撰寫成 React Component
  2. 建立 HOC 檔案,並撰寫草稿
  3. 將想要重複使用的程式碼或邏輯搬移到 HOC 檔案中
  4. 將 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 傳入

參考