Render Props - New pattern in React

weixin_34239169發表於2018-03-04

在軟體開發的過程中Code Reuse和可讀性一直是開發人員致力於解決的問題,在React社群中,以往出現了很多的Pattern來解決這個問題,例如Mixin,HOC等等。Render Props是React社群中裡提出的另外的一種Pattern。由React Router的Co-author Michael Jackson 提出,它與HOC等有什麼區別呢?又解決了什麼問題呢?在這篇文章來一探究竟。

問題的引出

所有的Pattern或者solution都是為了解決問題而提出的,那麼render props到底是為了解決什麼問題呢?我們先來看一個例子。例如在我們的App裡我們實現了這樣一個component

import React from 'react';

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

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

  render() {
    return (
      <div style={{ height: 800 }} onMouseMove={this.handleMouseMove}>
        <p>
          The mouse is at ({this.state.x}, {this.state.y})
        </p>
      </div>
    );
  }
}    
複製程式碼

Code Sandbox

熟悉React的同學都知道這是一個很簡單的App,他可以追蹤滑鼠位置給到自己。但是問題來了,萬一哪天你的朋友看到這個component,說這個feature很Cool,他的app裡能不能用上呢?那麼該怎麼解決這個問題呢?

Code Repeat

首先第一種想法很直接,可以把這個component做為一個父Component,另外一個Component作為一個子Component。來看程式碼。

import React from 'react';

const Dog = ({mouse}) => (
    <div>{mouse.x}, {mouse.y}</div>
)

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

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

  render() {
    return (
      <div style={{ height: 800 }} onMouseMove={this.handleMouseMove}>
        <p>
          The mouse is at ({this.state.x}, {this.state.y})
          <Dog mouse={mouse} />
        </p>
      </div>
    );
  }
}
複製程式碼

現在我們有了兩個Component, Dog 和 DogWithMouse,其實Dog作為DogWithMouse的子Component,那麼DogWithMouse Component就能利用滑鼠位置。好像這樣是把問題解決了,但是如果我們有另外一個Component也想要trace mouse這個feature怎麼辦呢?難道再重新這樣寫一個Component嗎?顯然這種方式是不可取的。

HOC

如果熟悉react的同學,可能會想到,這很簡單呀,我們寫一個High order Component(HOC)就行了,的確HOC可以解決這個問題,我們來看看HOC的程式碼是怎麼樣的。


import React from 'react';

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

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

    render() {
      return (
        <div style={{ height: 800 }} onMouseMove={this.handleMouseMove}>
          <Component mouse={this.state} {...this.props} />
        </div>
      );
    }
  }
}
const Dog = ({ mouse }) => <div>{mouse.x}, {mouse.y}</div>

const EnhancedDog = withMouse(Dog)

複製程式碼

在上面的程式碼裡,我們建立了一個withMouse的HOC, 這樣就把這部分邏輯抽象了出來。其他的Component想要使用我們mouse tracking的feature。只要將Component傳入HOC就能得到一個enhance過的Component。pretty cool, right?如果對於HOC不太瞭解,可以參考react 的官方文件進行了解。 HOC

HOC似乎已經完美的解決了問題,但是這種方式有沒有什麼缺點呢?似乎是有下面的一些缺點。

  1. 間接的props傳入。 讓我們重新看一看Dog Component,他的props傳入了mouse,但是在使用這個Component的時候,並沒有地方傳入了mouse。這是因為mouse是在HOC裡傳入的。你大概會納悶這個Mouse是從那來的。如果維護一個大專案的時候,有時候你就會發現這種間接的props傳入,是有多麼坑了。

  2. 命名衝突的問題。 用過react的同學不知道有沒有經歷過大量使用HOC的場景,筆者是見過多層HOC巢狀使用的專案程式碼。就像這樣: const NewComponent = withA(withB(withC(...(Component)))) 如果你用過這樣的多層巢狀的HOC,那麼就可能會發生命名衝突的問題。來看程式碼:


const withB = (Component) => {
  return class extends React.Component {
    render() {
      return (
        <div style={{ height: 800 }} >
          <Component mouse={'hehe'} {...this.props} />
        </div>
      );
    }
  }
}

const EnhancedDog = withB(withMouse(Dog))

複製程式碼

在這裡定義另外的一個HOC withB。它也給props上傳了一個同樣的mouse, 如果再用它來wrap withMouse(Dog) 時,你會發現mouse tracking的feature就不work,但是React不會有任何提示,這是命名衝突的問題。如果你有一個七八層的wrapp到一起的Component,如果出現這樣的命名衝突的問題,那麼就有的debug了。

Render props

好了,既然HOC有這樣的問題,那有沒有什麼方式既可以做到code reuse,同時也可以避免HOC的這些問題呢?答案就是Render Props。Render Props是React Traning團隊提出的一種新的React中組織Component的方式,同時也是十分的簡單,我們來看上邊的例子中,如果用render props如何解決code reuse的問題。

import React from 'react';
import { render } from "react-dom";

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

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

  render() {
    return (
      <div style={{ height: 800 }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

const DogMouse = () => (
  <Mouse render={({x, y}) => (<div>this mouose is ({x}, {y})</div>})>
)

render(<DogMouse/>, document.getElementById('app'))
複製程式碼

CodeSandBox

上邊就是render props的一個簡單例子,我們看完會發現特別的簡單,沒什麼特別的。Mouse就是一個普通的Component,唯一不同的是這個Component上定義了一個叫render的prop,是一個函式。在Mouse中會將state作為引數呼叫這個函式。而建立的DogMouse同樣也是一個普通的Component,其中定義render的方法,用於定義要render什麼Component。我們看看render props和HOC相比解決了什麼問題:

  1. code reuse。和HOC一樣的,render props同樣解決了code reuse的問題。把mouse tracking的feature抽象成為一個Mouse的Component,如果有另外一個Component像複用這個feature的時候,簡單的定義另外一個Component定義相應的render props就好了。

  2. 間接的props傳入,render props 沒有這個問題,可以清楚的看到props(x, y)是從什麼地方出入的.

  3. 命名衝突。利用render props是沒有Component wrapp的,所以除非定義Component時候自己命名重複,否則不會有命名衝突的問題。

從上面的例子上看,render props可以再大多數情況下替代HOC。react的官方文件上,目前也正式的介紹了render props,下次讓你想用HOC的時候,來試一試Render props把。

相關文章