跳至主要内容

[react] Higher Order Component(HOC)

內容可能過時

HOC 是 React 早期常用的程式碼重用模式。自 React 16.8 起,Custom Hooks 已成為邏輯重用的推薦方式,大多數 HOC 的使用場景都可以用 Custom Hooks 取代。此外,本文部分範例使用了 componentWillReceiveProps,該方法已在 React 18 中被移除。

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 傳入

參考