1. 程式人生 > >HoC 與 render props 相關要點

HoC 與 render props 相關要點

前者屬於 裝飾器, 後者屬於 控制反轉。其中, hoc 是一個接受元件作為引數,而返回一個新元件的純函式。render props 通過函式回撥的方式提供一個介面,用以渲染外部元件。

HoC

元件是可複用的, 元件中的邏輯(屬性、事件監聽、函式處理及渲染函式)可以抽離出來。對於一般的邏輯,最簡單的就是 utils 工具。對於包含狀態的邏輯,需要 hoc。通過 hoc 解決的是橫切關注點的問題。

render props

元件的複用,通過 props 傳遞值或回撥。如果 props 是一個函式並且返回值是一個元件,該外部元件將通過此元件間接渲染。這種操作是不改變 v-dom 樹的。

上層元件提供介面,下層元件實現介面。通過 props 提供介面,通過回撥函式去提供實現。

規則

HoC

需求:只有登入時才渲染元件

const withLogin = (Component) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <Component {...props} />;
    } else {
      return null;
    }
  }

  return NewComponent;
};

要點一、鏈式呼叫

// 高階元件裡使用生命週期函式
const withSection = Com => {
    class GetSection extends Component {
        state = {
            section: ''
        }
        componentDidMount() {
            // ajax
            const section = '章節 - 高階元件的class元件'
            this.setState({ section })
        }
        render() {
            return <Com {...this.props} section={this.state.section} />
        }
    }
    return GetSection
}

const withUserLog = Com => {
    class GetUserLog extends Component {
        componentDidMount() {
            console.log('記錄了使用者的閱讀時間操作')
        }
        render() {
            return <Com {...this.props} />
        }
    }
    return GetUserLog
}

// 鏈式呼叫
export default compose(withUserLog, withSection)(Book)
// <=> 等價於
export default withUserLog(withSection(Book))

要點二、函式柯里化

import React from 'react'
import PropTypes from 'prop-types'

const isEmpty = (prop) => (
    prop === null ||
    prop === undefined ||
    (prop.hasOwnProperty('length') && prop.length === 0) ||
    (prop.constructor === Object && Object.keys(prop).length === 0)
)

export default (loadingProp) => (WrappedComponent) => {
    const hocComponent = ({ ...props }) => {
        return isEmpty(props[loadingProp]) ? <div className="loader" /> : <WrappedComponent {...props} />
    }

    hocComponent.propTypes = {
    }

    return hocComponent
}

要點三、傳遞不相關的 props

export default (loadingProp) => (WrappedComponent) => {
    return class extends Component {
        componentDidMount() {
            this.startTimer = Date.now();
        }
        componentDidUpdate(prevProps, prevState) {
            if(!isEmpty(nextProps[loadingProp])) {
                this.endTimer = Date.now();
            }
        }

        render() {
            const myProps = {
                loaddingTime: ((this.endTimer - this.startTimer)/1000).toFixed(2),
            }
            return isEmpty(this.props[loadingProp]) ? <div className="loader" /> :
            <WrappedComponent {...this.props} {...myProps} />;
        }
    }
}

要點四、包裝顯示名稱,以便輕鬆除錯

高階元件不得不處理 displayName,不然 debug 會很痛苦。當 React 渲染出錯的時候,靠元件的 displayName 靜態屬性來判斷出錯的元件類,而高階元件總是創造一個新的 React 元件類,所以,每個高階元件都需要處理一下 displayName。

獲取檢視名的通用方法

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export default (loadingProp) => (WrappedComponent) => {
    class WithLoadingHoc extends Component {
        componentDidMount() {
            this.startTimer = Date.now();
        }
        componentDidUpdate(prevProps, prevState) {
            if(!isEmpty(nextProps[loadingProp])) {
                this.endTimer = Date.now();
            }
        }

        render() {
            const myProps = {
                loaddingTime: ((this.endTimer - this.startTimer)/1000).toFixed(2),
            }
            return isEmpty(this.props[loadingProp]) ? <div className="loader" /> :
            <WrappedComponent {...this.props} {...myProps} />;
        }
    }

    WithLoadingHoc.displayName = `WithLoadingHoc(${getDisplayName(WrappedComponent)})`;
    return WithLoadingHoc;
}

render props

傳統模式:低層元件直接呼叫高層元件(高 依賴與 低)

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          我們可以在這裡換掉 <p> 的 <Cat>   ......
          但是接著我們需要建立一個單獨的 <MouseWithSomethingElse>
          每次我們需要使用它時,<MouseWithCat> 是不是真的可以重複使用.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動滑鼠!</h1>
        <MouseWithCat />
      </div>
    );
  }
}

控制反轉模式: 高層提供介面,低層實現介面。高層不依賴於低層

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動滑鼠!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

結合 HoC,簡化呼叫

// 如果你出於某種原因真的想要 HOC,那麼你可以輕鬆實現
// 使用具有 render prop 的普通元件建立一個!
function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse render={mouse => (
          <Component {...this.props} mouse={mouse} />
        )}/>
      );
    }
  }
}

限制

HoC

要點一、複製靜態方法

hoc 會將原始元件將使用容器元件包裝成新元件,靜態方法通過原元件暴露,而新元件則缺少這些靜態方法。因此需要複製:

手動拷貝,你需要知道哪些方法應該被拷貝

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

這是因為 ref 不是 prop 屬性。就像 key 一樣,其被 React 進行了特殊處理。如果你對 HOC 新增 ref,該 ref 將引用最外層的容器元件,而不是被包裹的元件。

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}
/>;

使用轉發,向下傳遞

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 將自定義的 prop 屬性 “forwardedRef” 定義為 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回撥的第二個引數 “ref”。
  // 我們可以將其作為常規 prop 屬性傳遞給 LogProps,例如 “forwardedRef”
  // 然後它就可以被掛載到被 LogPros 包裹的子元件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

要點三、不要在 render 方法中使用HoC

因為 hoc 的作用是返回一個新的元件,如果直接在 render 中呼叫 hoc 函式,每次 render 都會生成新的元件。對於複用的目的來說,這毫無幫助,之前此外生成的舊元件因此被不斷解除安裝。

react 的 diff 演算法(稱為協調)使用元件標識來確定它是應該更新現有子樹還是將其丟棄並掛載新子樹。 如果從 render 返回的元件與前一個渲染中的元件相同(===),則 react 通過將子樹與新子樹進行區分來遞迴更新子樹。 如果它們不相等,則完全解除安裝前一個子樹

如果在元件之外建立 HOC,這樣一來元件只會建立一次。因此,每次 render 時都會是同一個元件。

render props

效能優化

箭頭函式影響效能

class Mouse extends React.PureComponent {
  // 與上面相同的程式碼......
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>

        {/*
          這是不好的!
          每個渲染的 `render` prop的值將會是不同的。
        */}
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

不直接使用箭頭函式

class MouseTracker extends React.Component {
  // 定義為例項方法,`this.renderTheCat`始終
  // 當我們在渲染中使用它時,它指的是相同的函式
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>