1.基本概念
高階組件是參數為組件,返回值為新組件的函數。
2.舉例說明
① 裝飾工廠模式
組件是 react 中的基本單元,組件中通常有一些邏輯(非渲染)需要復用處理。這里我們可以用高階組件對組件內部中的一些通用進行封裝。
未封裝時,相同的邏輯無法復用:
渲染評論列表
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> ); } }
渲染博客列表
lass 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} />; } }
借用高階組件,封裝公用邏輯:
const CommentListWithSubscription = withSubscription( CommentList, (DataSource) => DataSource.getComments() ); const BlogPostWithSubscription = withSubscription( BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id) );
組件加工(加工:處理公用邏輯)工廠,接受舊組件,返回新組件:
// 此函數接收一個組件... 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} />; } }; }
② 高階組件柯里化
高階組件是一個參數是組件返回值也是組件的函數,那么我們借助函數柯里化,確保最終返回的函數是高階組件就可以了。
import React, { Component } from "react"; const isEmpty = prop => (prop && prop.hasOwnProperty("length") && prop.length === 0) || (prop.constructor === Object && Object.keys(prop).length === 0); export default loadingProp => WrappedComponent => { const hocComponent = class extends Component { componentDidMount() { this.startTimer = Date.now(); } componentWillUpdate(nextProps, nextState) { console.log(nextProps) if (!isEmpty(nextProps[loadingProp])) { this.endTimer = Date.now(); } } render() { const myProps = { loadingTime: ((this.endTimer - this.startTimer) / 1000).toFixed(2) }; return isEmpty(this.props[loadingProp]) ? ( <div>loading...</div> ) : ( <WrappedComponent {...this.props} {...myProps} /> ); } }; return hocComponent; };
其中 ,而 loadingProp => ... 是一個返回值為高階組件的函數,其返回結果 WrappedComponent => {...} 是一個單參數(即被包裹的組件)的高階組件。這樣做有什么好處呢?為什么不寫一個高階組件並將參數與包裝組件一並傳遞過去?高階組件受限於返回值必須是組件,因此它無法柯里化。而在高階組件之上再構建一個函數就能進行柯里化。同時,返回的結果是單參數(被包裹組件)的高階組件可以直接做 hoc 嵌套,即 一個 hoc 嵌套 另一個 hoc(因為傳入值、傳出值都是 組件)。此外的對於 hoc 嵌套調用,可以借助 compose 工具函數 進行扁平化處理。
許多第三方庫提供都提供了
compose
工具函數,包括 lodash (比如lodash.flowRight
), Redux 和 Ramda。
③ 設定 hoc 的顯示名稱 displayName
為了方便調試,設定 hoc 的顯示名稱類似: WithSubscription(CommentList)
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription; } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }
3. 注意事項
① 不要在 render 方法中使用 hoc
因為 hoc 會產生新的組件,而 redner 方法經常被調用,所以會不斷產生新的組件(而在 react 的 diff 算法中會將之前舊的卸載而替換成新的,這不僅僅會對性能造成影響,同時重新掛載組件會導致該組件及其所有子組件的狀態丟失。
②務必復制靜態方法
手動一個個復制
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // 必須准確知道應該拷貝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
使用 hoist-non-react-statics 自動拷貝所有非 React 靜態方法
import hoistNonReactStatic from 'hoist-non-react-statics'; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; }
③ Ref 需要轉發
雖然高階組件的約定是將所有 props 傳遞給被包裝組件,但這對於 refs 並不適用。那是因為 ref
實際上並不是一個 prop - 就像 key
一樣,它是由 React 專門處理的。如果將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。
使用 hoc 包裹組件
class FancyButton extends React.Component { focus() { // ... } // ... } // 我們導出 LogProps,而不是 FancyButton。 // 雖然它也會渲染一個 FancyButton。 export default logProps(FancyButton);
形式上導入的是原組件,實際上導入的是 hoc 包裹的原組件。這時如果直接傳 ref 到該組件,實際上 ref 並沒有傳遞到原組件中,而停留在 hoc 組件上。
import FancyButton from './FancyButton'; const ref = React.createRef(); // 我們導入的 FancyButton 組件是高階組件(HOC)LogProps。 // 盡管渲染結果將是一樣的, // 但我們的 ref 將指向 LogProps 而不是內部的 FancyButton 組件! // 這意味着我們不能調用例如 ref.current.focus() 這樣的方法 <FancyButton label="Click Me" handleClick={handleClick} ref={ref} />;
這里我們需要對停留在 hoc 組件中的 ref 進行轉發,使其傳遞到原組件中
return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; });
233