[譯]React高級話題之Render Props

前言

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

原文地址:Render Propshtml

render prop是一個技術概念。它指的是使用值爲function類型的prop來實現React component之間的代碼共享。java

若是一個組件有一個render屬性,而且這個render屬性的值爲一個返回React element的函數,而且在組件內部的渲染邏輯是經過調用這個函數來完成的。那麼,咱們就說這個組件使用了render props技術。react

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

很多類庫都使用了這種技術,好比說:React RouterDownshiftgit

在這個文檔裏面,咱們將會討論爲何render props是如此有用,你該如何編寫本身的render props組件。github

正文

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

在React中,組件是代碼複用的基本單元(又來了,官方文檔不斷地在強調這個準則)。到目前爲止,在React社區裏面,關於共享state或者某些類似的行爲(好比說,將一個組件封裝進另外一擁有相同state的組件)尚未一個明朗的方案。web

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

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軸上的座標值。react-router

那麼問題來了: 咱們該如何在別的組件複用這種行爲(指的是監聽mouseMove事件,獲取光標的座標值)呢?換句話說,若是別的組件也須要知道目前光標的座標值,那咱們能不能將這種行爲封裝好,而後在另一個組件裏面開箱即用呢?ide

由於,在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了。

相關文章
相關標籤/搜索