React HOC高階組件詳解

High Order Component(包裝組件,後面簡稱HOC),是React開發中提升組件複用性的高級技巧。HOC並非React的API,他是根據React的特性造成的一種開發模式。html

HOC具體上就是一個接受組件做爲參數並返回一個新的組件的方法前端

const EnhancedComponent = higherOrderComponent(WrappedComponent)
複製代碼

在React的第三方生態中,有很是多的使用,好比Redux的connect方法或者React-Router的withrouter方法。react

舉個例子

咱們有兩個組件:git

// CommentList
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); } } 複製代碼
// BlogPost
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />; } } 複製代碼

他們雖然是兩個不一樣的組件,對DataSource的需求也不一樣,可是他們有不少的內容是類似的:github

  • 在組件渲染以後監聽DataSource
  • 在監聽器裏面調用setState
  • 在unmout的時候刪除監聽器

在大型的工程開發裏面,這種類似的代碼會常常出現,那麼若是有辦法把這些類似代碼提取並複用,對工程的可維護性和開發效率能夠帶來明顯的提高。算法

使用HOC咱們能夠提供一個方法,並接受不了組件和一些組件間的區別配置做爲參數,而後返回一個包裝過的組件做爲結果。前端工程師

function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />; } }; } 複製代碼

而後咱們就能夠經過簡單的調用該方法來包裝組件:app

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
複製代碼

注意:在HOC中咱們並無修改輸入的組件,也沒有經過繼承來擴展組件。HOC是經過組合的方式來達到擴展組件的目的,一個HOC應該是一個沒有反作用的方法。框架

在這個例子中咱們把兩個組件類似的生命週期方法提取出來,並提供selectData做爲參數讓輸入組件能夠選擇本身想要的數據。由於withSubscription是個純粹的方法,因此之後若是有類似的組件,均可以經過該方法進行包裝,可以節省很是多的重複代碼。post

不要修改原始組件,使用組合進行功能擴展

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);
複製代碼

經過以上方式咱們也能夠達到擴展組件的效果,可是會存在一些問題

  • 若是InputComponent自己也有componentWillReceiveProps生命週期方法,那麼就會被覆蓋
  • functional component不適用,由於他根本不存在生命週期方法

修改原始組件的方式缺少抽象化,使用者必須知道這個方法是如何實現的來避免上面提到的問題。

若是經過組合的方式來作,咱們就能夠避免這些問題

function logProps(InputComponent) {
  return class extends React.Component{
    componentWillReceiveProps(nextProps) {
        console.log('Current props: ', this.props);
        console.log('Next props: ', nextProps);
    }
    render() {
        <InputComponent {...this.props} />
    }
  }
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
複製代碼

慣例:無關的props傳入到原始組件

HOC組件會在原始組件的基礎上增長一些擴展功能使用的props,那麼這些props就不該該傳入到原始組件(固然有例外,好比HOC組件須要使用原始組件指定的props),通常來講咱們會這樣處理props:

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); } 複製代碼

extraProp是HOC組件中要用的props,不用的剩下的props咱們都認爲是原始組件須要使用的props,若是是二者通用的props你能夠單獨傳遞。

慣例:包裝組件的顯示名稱來方便調試

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製代碼

簡單來講就是經過手動指定displayName來讓HOC組件可以更方便得被react devtool觀察到

慣例:不要在render方法裏面調用HOC方法

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 />; } 複製代碼

一來每次調用enhance返回的都是一個新的class,react的diffing算法是根據組件的特徵來判斷是否須要從新渲染的,若是兩次render的時候組件之間不是(===)徹底相等的,那麼會直接從新渲染,而部署根據props傳入以後再進行diff,對性能損耗很是大。而且從新渲染會讓以前的組件的state和children所有丟失。

二來React的組件是經過props來改變其顯示的,徹底沒有必要每次渲染動態產生一個組件,理論上須要在渲染時自定義的參數,均可以經過事先指定好props來實現可配置。

靜態方法必須被拷貝

有時候會在組件的class上面外掛一下幫助方法,若是按照上面的方法進行包裝,那麼包裝以後的class就沒有來這些靜態方法,這時候爲了保持組件使用的一致性,通常咱們會把這些靜態方法拷貝到包裝後的組件上。

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
複製代碼

這個之適用於你已知輸入組件存在那些靜態方法的狀況,若是須要可擴展性更高,那麼能夠選擇使用第三方插件hoist-non-react-statics

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
複製代碼

ref

ref做爲React中的特殊屬性--相似於key,並不屬於props,也就是說咱們使用傳遞props的方式並不會把ref傳遞進去,那麼這時候若是咱們在HOC組件上放一個ref,拿到的是包裝以後的組件而不是原始組件,這可能就會致使一些問題。

在React 16.3以後官方增長來一個React.forwardRef方法來解決這個問題,具體能夠參考這裏

我是Jocky,一個專一於React技巧和深度分析的前端工程師,React絕對是一個越深刻學習,越能讓你以爲他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裏

相關文章
相關標籤/搜索