[譯]React高階話題之Render Props

鯊叔發表於2018-11-29

前言

本文為意譯,翻譯過程中摻雜本人的理解,如有誤導,請放棄繼續閱讀。

原文地址:Render Props

render prop是一個技術概念。它指的是使用值為function型別的prop來實現React component之間的程式碼共享。

如果一個元件有一個render屬性,並且這個render屬性的值為一個返回React element的函式,並且在元件內部的渲染邏輯是通過呼叫這個函式來完成的。那麼,我們就說這個元件使用了render props技術。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>
複製程式碼

不少類庫都使用了這種技術,比如說:React RouterDownshift

在這個文件裡面,我們將會討論為什麼render props是如此有用,你該如何編寫自己的render props元件。

正文

使用Render Props來完成關注點分離

在React中,元件是程式碼複用的基本單元(又來了,官方文件不斷地在強調這個準則)。到目前為止,在React社群裡面,關於共享state或者某些相似的行為(比如說,將一個元件封裝進另一擁有相同state的元件)還沒有一個明朗的方案。

舉個例子,下面這個元件是用於在web應用中追蹤滑鼠的位置:

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

複製程式碼

隨著游標在螢幕上面移動,這個元件將會在文件的<p>標籤裡面顯示當前游標在x,y軸上的座標值。

那麼問題來了: 我們該如何在別的元件複用這種行為(指的是監聽mouseMove事件,獲取游標的座標值)呢?換句話說,如果別的元件也需要知道目前游標的座標值,那我們能不能將這種行為封裝好,然後在另外一個元件裡面開箱即用呢?

因為,在React中,元件是程式碼複用的基本單元(again)。那好,我們一起來重構一下程式碼,把我們需要複用的行為封裝到<Mouse>元件當中。

// The <Mouse> component encapsulates the behavior we need...
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}>

        {/* ...but how do we render something other than a <p>? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}
複製程式碼

現在,<Mouse>元件看似把所有跟監聽mousemove事件,儲存游標的座標值等相關的行為封裝在一起了。實際上,它還不能達到真正的可複用。

假設,我們需要實現這麼一個元件。它需要渲染出一隻用圖片表示的貓去追逐游標在螢幕上移動的視覺效果。我們可能會通過向<Cat>元件傳遞一個叫mouse(它的值為{{x,y}})的prop來獲得當前游標所在位置。

首先,我們會在<Mouse>元件的render方法裡面插入這個<Cat>元件,像這樣子:

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

        {/*
          We could just swap out the <p> for a <Cat> here ... but then
          we would need to create a separate <MouseWithSomethingElse>
          component every time we need to use it, so <MouseWithCat>
          isn't really reusable yet.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

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

這種方式的實現可能對個別的場景有用,但是,我們還是沒有達成通過封裝讓這種行為真正地複用的目標。在別的應用場景下,每一次當我們需要獲取游標在螢幕上的座標的時候,我們都需要重新建立一個元件(例如,一個跟<MouseWithCat>相似元件)來完成這個業務場景所對應的渲染任務。

這個時候,就輪到render props 出場啦:相比直接把<Cat>這個元件硬編碼到<Mouse>元件當中,刻意地去改變<Mouse>元件的UI輸出(也就是我們重新定義一個<MouseWithCat>元件的原因)。更好的做法是,我們可以給<Mouse>元件定義一個值為函式型別的prop,讓這個prop自己來動態地決定要在Mouse元件的render方法要渲染東西。這個值為函式型別的prop就是我們所說的render prop了。

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>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}
複製程式碼

現在,相比每一次都要重複地將<Mouse>元件的程式碼複製一遍,然後將我們要渲染的東西硬編碼到<Mouse>的render方法中去,我們採取了一個更省力的辦法。那就是給Mouse新增了一個render屬性,讓這個屬性來決定要在<Mouse>元件中渲染什麼。

更加具體和直白地說,一個render prop(這裡不是代指技術,而是元件屬性) 就是一個值為函式型別的prop。通過這個函式,我們讓掛載了這個prop的元件知道自己要去渲染什麼

這種技術使得我們之前想要共享的某些行為(的實現)變得非常之可移植(portable)。假如你想要得到這種行為,你只需要渲染一個帶render屬性的類<Mouse>元件到你的元件樹當中就可以了。剩下的就讓這個render prop來獲取相關的資料(通過函式形參被例項化時得到。拿上述例子來說,就是(mouse)=> <Cat mouse={mouse}>mouse),然後決定如何幹預這個元件的渲染。

一個很有意思的,並值得我們注意的事情是,你完全可以通過一個帶render屬性的普通元件來實現大部分的HOC。舉個例子,假如你在共享行為(監聽mousemove事件,獲得游標在螢幕上的座標)時不想通過<Mouse>元件來完成,而是想通過高階元件withMouse來完成的話,那麼就可以很簡單地通過建立一個帶render prop的<Mouse>元件來達成:

// If you really want a HOC for some reason, you can easily
// create one using a regular component with a render prop!
function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse render={mouse => (
          <Component {...this.props} mouse={mouse} />
        )}/>
      );
    }
  }
}
複製程式碼

可以這麼說,render props(指技術)讓HOC技術與其他技術(在這裡,指它自己)的組合使用成為了可能。

render prop的prop名不一定叫“render”

如上面的標題,你要牢牢記住,這種技術雖然叫render props,但是prop屬性的名稱不一定非得叫“render”。實際上,只要元件上的某個屬性值是函式型別的,並且這個函式通過自己的形參例項化時獲取了這個元件的內部資料,參與到這個元件的UI渲染中去了,我們就說這個元件應用了render props這種技術。

在上面的例子當中,我們一直在使用“render”這個名稱。實際上,我們也可以輕易地換成children這個名稱!

<Mouse children={mouse => (
  <p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
複製程式碼

同時,我們也要記住,這個“children”prop不一定非得羅列在在JSX element的“屬性”列表中。它實際上就是我們平時用JSX宣告元件時的children,因此你也可以像以前一樣把它放在元件的內部。

<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>
複製程式碼

react-motion這個庫的API中,你會看到這種寫法的應用。

因為這種寫法比較少見,所以假如你這麼做了,為了讓看你程式碼的人不產生疑惑的話,你可能需要在靜態屬性propTypes中顯式地宣告一下children的資料型別必須為函式。

Mouse.propTypes = {
  children: PropTypes.func.isRequired
};
複製程式碼

注意點

當跟React.PureComponent結合使用時,要當心

如果你在元件的render方法裡面建立了一個函式的話,然後把這個函式賦值給這個元件的prop的話,那麼得到的結果很有可能是違背了你初衷的。怎麼說呢?因為一旦你這麼做了,React在作shallow prop comparison的時候,new props都會被判斷為不等於old props的。現實是,這麼做恰恰會導致在每一次render的呼叫的時候生成一個新的值給這個屬性。

我們繼續拿上面的<Mouse>元件作為例子。假如<Mouse>元件繼承了React.PureComponent的話,我們的程式碼應該是像下面這樣的:

class Mouse extends React.PureComponent {
  // Same implementation as above...
}

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

        {/*
          This is bad! The value of the `render` prop will
          be different on each render.
        */}
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}
複製程式碼

在上面的程式碼例子當中,每一次<MouseTracker>元件的render方法被呼叫的時候,它都會生成一個新的函式例項給<Mouse>元件,作為“render”屬性的值。然而,我們之所以繼承React.PureComponent,就是想減少<Mouse>元件被渲染的次數。如此一來,<Mouse>因為一個新的函式例項被迫判定為props已經發生改變了,於是乎進行了不必要的渲染。這與我們的讓<Mouse>元件繼承React.PureComponent的初衷是相違背的。

為了避開(To get around)這個問題,你可以把render prop的值賦值為<MouseTracker>元件例項的一個方法,這樣:

class MouseTracker extends React.Component {
  // Defined as an instance method, `this.renderTheCat` always
  // refers to *same* function when we use it in render
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

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

在某些場景下,你可能無法把prop的值靜態地賦值為元件例項的某個方法(例如,你需要覆蓋元件的props值或者state值,又兩者都要覆蓋)。那麼,在這種情況下,你只能老老實實地讓<Mouse>元件去繼承React.Component了。

相關文章