天天看點

高階元件(精讀React官方文檔—19)

核心

  1. 高階元件不用關心資料是如何渲染的,隻用關心邏輯即可。
  2. 被包裝的元件不用關心資料是怎麼來的,隻用負責渲染即可。
  3. 最後渲染的是高階元件傳回的元件。
一個例子看懂高階元件的用法
高階元件(HOC)是React中用于複用元件邏輯的一種進階技巧,HOC是基于React的組合特性而形成的設計模式。

解讀

  • 高階元件是一種函數,接收的參數是元件,傳回的也是元件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
複制代碼      

使用HOC解決橫切關注點問題

元件是React中代碼複用的基本單元,但是有些模式并不适合傳統元件。
  • 下面的元件訂閱外部資料源,用以渲染評論清單,元件名為CommentList
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假設 "DataSource" 是個全局範圍内的資料源變量
      comments: DataSource.getComments()
    };
  }
  componentDidMount() {
    // 訂閱更改
    DataSource.addChangeListener(this.handleChange);
  }
  componentWillUnmount() {
    // 清除訂閱
    DataSource.removeChangeListener(this.handleChange);
  }
  handleChange() {
    // 當資料源更新時,更新元件狀态
    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} />;
  }
}
複制代碼      

上面兩個元件的相同點

  1. 在挂載時,向DataSource添加一個偵聽器。
  2. 在偵聽器内部,當資料源發生變化變化時,調用setState。
  3. 在解除安裝時,删除偵聽器。

上面兩個元件的不同點

  1. DataSource上調用不同的方法。
上面兩個元件相同點的地方被不斷的重複調用,在大型項目中,是以我們需要将這些共同使用的地方給抽象出來,然後讓許多元件之間共享它,這正是高階元件擅長的地方。
  • 編寫一個建立元件函數,這個函數接收兩個參數,一個是要被包裝的子元件,另一個則是該子元件訂閱資料的函數。
const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
複制代碼      
  • 當渲染 CommentListWithSubscription 和 BlogPostWithSubscription 時, CommentList 和 BlogPost 将傳遞一個 data prop,其中包含從 DataSource 檢索到的最新資料.
// 此函數接收一個元件...
function withSubscription(WrappedComponent, selectData) {
  // ...并傳回另一個元件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    componentDidMount() {
      // ...負責訂閱相關的操作...
      DataSource.addChangeListener(this.handleChange);
    }
    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }
    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }
    render() {
      // ... 并使用新資料渲染被包裝的元件!
      // 請注意,我們可能還會傳遞其他屬性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
複制代碼      
HOC不會修改傳入的元件,也不會使用繼承來複制其行為,相反HOC是通過将元件包裝在容器元件中來組成新的元件,HOC是純函數,沒有副作用。
  1. 被包裝元件接收來自容器元件的所有prop,同時也接收一個新的用于render的data prop
  2. HOC不用關心資料的使用方式,被包裝元件也不用關心資料是怎麼來的。

不要改變原始元件,使用組合

  • 不要試圖在HOC中修改元件原型
function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function(prevProps) {
    console.log('Current props: ', this.props);
    console.log('Previous props: ', prevProps);
  };
  // 傳回原始的 input 元件,暗示它已經被修改。
  return InputComponent;
}
// 每次調用 logProps 時,增強元件都會有 log 輸出。
const EnhancedComponent = logProps(InputComponent);
複制代碼      
  • 上面這種寫法會造成另一個同樣會修改componentDidUpate的HOC增強它,那麼前面的HOC就會失效。
  • HOC不應該修改傳入元件,而應該使用組合的方式,将元件包裝在容器元件中實作功能。
function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    render() {
      // 将 input 元件包裝在容器中,而不對其進行修改。Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}
複制代碼      

約定:将不相關的props傳遞給被包裹的元件

HOC為元件添加特性,自身不應該大幅改變約定,HOC應該透傳與自身無關的props,大多數HOC都應該包含一個類似于下面的render方法
render() {
  // 過濾掉非此 HOC 額外的 props,且不要進行透傳
  const { extraProp, ...passThroughProps } = this.props;
  // 将 props 注入到被包裝的元件中。
  // 通常為 state 的值或者執行個體方法。
  const injectedProp = someStateOrInstanceMethod;
  // 将 props 傳遞給被包裝元件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
複制代碼      

約定:最大化可組合性

  • 有時候HOC僅僅接收一個參數,也就是被包裹的元件
const NavbarWithRouter = withRouter(Navbar);
複制代碼      
  • HOC通常可以接收多個參數
const CommentWithRelay = Relay.createContainer(Comment, config);
複制代碼      
  • React Redux的connect函數
// React Redux 的 `connect` 函數
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
複制代碼      
  • 讓我們将connect函數進行拆分
// connect 是一個函數,它的傳回值為另外一個函數。
const enhance = connect(commentListSelector, commentListActions);
// 傳回值為 HOC,它會傳回已經連接配接 Redux store 的元件
const ConnectedComment = enhance(CommentList);
複制代碼      
總結:connect就是一個傳回高階元件的高階函數。這個函數可能看起來難懂,但是實際上這個函數傳回了一個高階元件,然後這個高階元件接收一個元件作為參數,最後再傳回一個元件。
// 而不是這樣...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... 你可以編寫組合工具函數
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
  // 這些都是單參數的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複制代碼      

約定:包裝顯示名稱以便輕松調試

HOC建立的容器元件會和任何其他元件一樣,顯示在React Developer Tools中,為了友善調試,需要選擇顯示一個名稱,以表明他是HOC的産物。
function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複制代碼      

使用高階元件的注意事項

  1. 不要在render方法中使用HOC
render() {
  // 每次調用 render 函數都會建立一個新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 這将導緻子樹每次渲染都會進行解除安裝,和重新挂載的操作!
  return <EnhancedComponent />;
}
複制代碼      
  1. 務必複制靜态方法
// 定義靜态函數
WrappedComponent.staticMethod = function() {/*...*/}
// 現在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增強元件沒有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
複制代碼      
為了解決上述的問題,可以在傳回之前把這些方法拷貝到容器元件上:
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必須準确知道應該拷貝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
複制代碼      
  1. refs不會被傳遞
雖然高階元件的約定式将所有props傳遞給包裝元件,但是這對refs并不适用,因為ref實際上并不是一個prop,關于這個問題,我的專欄裡前面介紹ref的講過這個問題。

歡迎大家跟着我的專欄,一起學習React!

繼續閱讀