React進階系列: Render Props 從介紹到實踐

Anx發表於2018-02-02

什麼是render props

render props是一個元件間共享程式碼邏輯的小技巧, 通過props傳遞函式來實現。有許多庫(比如React Router, React Motion)都使用了這個技巧。

元件有一個叫做renderprop, 值是一個返回React元素的函式, 在元件內部呼叫這個函式渲染元件。

語言描述不夠直觀, 來一個例子:

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

有什麼用?

從一個例子開始, 假設我們需要一個追蹤滑鼠位置的元件:

class MouseTracker 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}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

這個元件能跟隨著滑鼠移動顯示座標, 現在問題是: 我們怎麼將這個邏輯複用到其他元件中?比如我們需要一個跟隨滑鼠位置顯示圖片的元件。換句話說: 我們怎麼將追蹤滑鼠位置封裝為可供其他元件複用的邏輯?

高階元件(HOC)似乎是一個辦法:

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

        {/*
          我們可以把希望渲染的東西放在這裡
          但是如果希望實現其他功能
          每次都需要建立一個<MouseWithSomethingElse>的新元件
          所以這不是真正的複用
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

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

使用這種方法在特定用途下才有用, 並沒有真正的將功能封裝成可複用的邏輯, 每次我們需要一個新的追蹤滑鼠位置的用例, 都要建立一個新的<MouseWithSomethingElse>元件, 而這些元件間的追蹤滑鼠位置的程式碼是冗餘的。

這個時候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 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}>

        {/*
          使用傳入函式的邏輯動態渲染
          而不是硬編碼地渲染固定內容
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

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

現在我們可以隨處複用追蹤滑鼠的邏輯而不用寫重複的程式碼或者建立特定用途的元件了, 這才是真正的邏輯複用。

使用其他props

雖然這個技巧或者說模式(Pattern)叫Render Props, 但是並不一定要使用render來傳遞渲染函式, 你甚至可以使用children:

<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>

這正是React Motion庫使用的API。

(瞭解過另一個React模式 – Render callback的朋友會發現這時候就跟這個模式一樣了。)

使用中的注意事項

當與React.PureComponent一起使用時要注意

對React熟悉的朋友都知道(不熟悉的可以看這篇文章), 當props或者state變化時, React預設都會重新渲染, 有兩種方法來避免不必要的重複渲染:

  1. 手動實現shouldComponentUpdate方法
  2. 繼承React.PureComponent, 這時會自動進行淺比較

但是當你使用Render Props時, 每次傳入的render都是一個新的函式, 所以每次淺比較都會導致重新渲染。

為了避免這個問題, 你可以將prop定義為一個例項方法:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);

    // 在這裡繫結來避免每次傳入一個新的例項
    this.renderTheCat = this.renderTheCat.bind(this);
  }

  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

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

實踐

介紹了用法用途和注意事項之後, 我們來看在實際專案中如何使用。

一個很好的例子是動畫效果, 因為絕大多數動效都是可以複用的, 並且動畫效果要渲染的內容需要根據實際使用場景變化, 很難複用, react-motion這個庫很好的實現了一套覆蓋大多數使用場景的複用方法。

react-motion使用起來非常簡單直觀, 由庫來提供動畫效果的變化, 使用者來編寫實際需要渲染的內容。

import {Motion, spring} from `react-motion`;

<Motion style={{x: spring(this.state.open ? 400 : 0)}}>
  {({x}) =>
    <div className="demo0">
      <div className="demo0-block" style={{
        WebkitTransform: `translate3d(${x}px, 0, 0)`,
        transform: `translate3d(${x}px, 0, 0)`,
      }} />
    </div>
  }
</Motion>

react motion原始碼中的關鍵部分:

render(): ReactElement {
  // 呼叫通過children傳入的渲染函式, 將this.state.currentStyle作為引數傳入
  const renderedChildren = this.props.children(this.state.currentStyle);
  return renderedChildren && React.Children.only(renderedChildren);
}

componentDidMount() {
  this.prevTime = defaultNow();
  // 元件掛載後執行關鍵的動畫效果函式
  // 動畫效果函式封裝的是可複用動畫處理邏輯
  // 根據時間變化動態地對this.state.currentStyle更新
  this.startAnimationIfNecessary();
}

react motion將動畫效果從每個元件硬編碼的邏輯抽離出來並封裝成可複用的庫, 庫僅負責對動畫變換過程中的邏輯運算, 實際的渲染由使用者定義, 使用者根據庫計算出來的數值來渲染, 在覆蓋大多數使用場景的同時實現了最大程度的複用行。

總結

Render Props是一種複用通用邏輯的模式, 在實際開發中應該根據實際場景選擇合適的方法/模式(不管是HOC還是Render Props), 最大程度地利用React的複用性, 保持程式碼DRY。

Render Props已經出現在了React官方文件的Advance Guide中, 並且有許多的開源庫(點選這裡檢視)使用它來設計API實現。

參考

React官方文件
Github – react-motion

相關文章