[譯] 使用 Render props 吧!

更新我提交了一個 PR 到 React 官方文檔,爲其添加了 Render propshtml

更新2:添加一部份內容來講明 「children 做爲一個函數」 也是相同的概念,只是 prop 名稱不一樣罷了。前端


幾個月前,我發了一個 twitter:react

譯註:@reactjs 我能夠在一個普通組件上使用一個 render prop 來完成 HOC(高階組件) 可以作到的事情。不服來辯。android

我認爲,高階組件模式 做爲一個在許多基於 React 的代碼中流行的代碼複用手段,是能夠被一個具備 「render prop」 的普通組件 100% 地替代的。「不服來辯」 一詞是我對 React 社區朋友們的友好 「嘲諷」,隨之而來的是一個系列好的討論,但最終,我對我本身沒法用 140 字來完整描述我想說的而感到失望。 我 決定在將來的某個時間點寫一篇更長的文章 來公平公正的探討這個主題。ios

兩週前,當 Tyler 邀請我到 Phoenix ReactJS 演講時,我認爲是時候去對此進行更進一步的探討了。那周我已經到達 Phoenix 去啓動 咱們的 React 基礎和進階補習課 了,並且我還從個人商業夥伴 Ryan 聽到了關於大會的好消息,他在四月份作了演講git

在大會上,個人演講彷佛有點標題黨的嫌疑:不要再寫另外一個 HOC 了。你能夠在 Phoenix ReactJS 的 YouTube 官方頻道 上觀看個人演講,也能夠經過下面這個內嵌的視頻進行觀看:github

若是你不想看視頻的話,能夠閱讀後文對於演講主要內容的介紹。可是嚴肅地說:視頻要有趣多了 😀。typescript

若是你直接跳過視頻開始閱讀,但並無領會我所說的意思,就折回去看視頻吧。演講時的細節會更豐富。後端

Mixins 存在的問題

個人演講始於高階組件主要解決的問題:代碼複用api

讓咱們回到 2015 年使用 React.createClass 那會兒。假定你如今有一個簡單的 React 應用須要跟蹤並在頁面上實時顯示鼠標位置。你可能會構建一個下面這樣的例子:

import React from 'react'
import ReactDOM from 'react-dom'

const App = React.createClass({
  getInitialState() {
    return { x: 0, y: 0 }
  },

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

  render() {
    const { x, y } = this.state

    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))
複製代碼

如今,假定咱們在另外一個組件中也須要跟蹤鼠標位置。咱們能夠重用 <App> 中的代碼嗎?

createClass 這個範式中,代碼重用問題是經過被稱爲 「mixins」 的技術解決的。咱們建立一個 MouseMixin,讓任何人都能經過它來追蹤鼠標位置。

import React from 'react'
import ReactDOM from 'react-dom'

// mixin 中含有了你須要在任何應用中追蹤鼠標位置的樣板代碼。
// 咱們能夠將樣板代碼放入到一個 mixin 中,這樣其餘組件就能共享這些代碼
const MouseMixin = {
  getInitialState() {
    return { x: 0, y: 0 }
  },

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

const App = React.createClass({
  // 使用 mixin!
  mixins: [ MouseMixin ],
  
  render() {
    const { x, y } = this.state

    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div>
    )
  }
})

ReactDOM.render(<App/>, document.getElementById('app'))
複製代碼

問題解決了,對吧?如今,任何人都能輕鬆地將 MouseMixin 混入他們的組件中,並經過 this.state 屬性得到鼠標的 xy 座標。

HOC 是新的 Mixin

去年,隨着ES6 class 的到來,React 團隊最終決定使用 ES6 class 來代替 createClass。這是一個明智的決定,沒有人會在 JavaScript 都內置了 class 時還會維護本身的類模型。

但就存在一個問題:ES6 class 不支持 mixin。除了不是 ES6 規範的一部分,Dan 已經在一篇 React 博客上發佈的博文上詳細討論了 mixin 存在的其餘問題。

minxins 的問題總結下來就是

  • ES6 class。其不支持 mixins。
  • 不夠直接。minxins 改變了 state,所以也就很難知道一些 state 是從哪裏來的,尤爲是當不止存在一個 mixins 時。
  • 名字衝突。兩個要更新同一段 state 的 mixins 可能會相互覆蓋。createClass API 會對兩個 mixins 的 getInitialState 是否具備相同的 key 作檢查,若是具備,則會發出警告,但該手段並不牢靠。

因此,爲了替代 mixin,React 社區中的很多開發者最終決定用高階組件(簡稱 HOC)來作代碼複用。在這個範式下,代碼經過一個相似於 裝飾器(decorator) 的技術進行共享。首先,你的一個組件定義了大量須要被渲染的標記,以後用若干具備你想用共享的行爲的組件包裹它。所以,你如今是在 裝飾 你的組件,而不是混入你須要的行爲!

import React from 'react'
import ReactDOM from 'react-dom'

const withMouse = (Component) => {
  return class extends React.Component {
    state = { x: 0, y: 0 }

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

    render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <Component {...this.props} mouse={this.state}/> </div> ) } } } const App = React.createClass({ render() { // 如今,咱們獲得了一個鼠標位置的 prop,而再也不須要維護本身的 state const { x, y } = this.props.mouse return ( <div style={{ height: '100%' }}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } }) // 主須要用 withMouse 包裹組件,它就能得到 mouse prop const AppWithMouse = withMouse(App) ReactDOM.render(<AppWithMouse/>, document.getElementById('app')) 複製代碼

讓咱們和 mixin 說再見,去擁抱 HOC 吧。

在 ES6 class 的新時代下,HOC 的確是一個可以優雅地解決代碼重用問題方案,社區也已經普遍採用它了。

此刻,我想問一句:是什麼驅使咱們遷移到 HOC ? 咱們是否解決了在使用 mixin 時遇到的問題?

讓咱們看下:

  • ES6 class。這裏再也不是問題了,ES6 class 建立的組件可以和 HOC 結合。
  • 不夠直接。即使用了 HOC,這個問題仍然存在。在 mixin 中,咱們不知道 state 從何而來,在 HOC 中,咱們不知道 props 從何而來。
  • 名字衝突。咱們仍然會面臨該問題。兩個使用了同名 prop 的 HOC 將遭遇衝突而且彼此覆蓋,而且此次問題會更加隱晦,由於 React 不會在 prop 重名是發出警告。

另外一個 HOC 和 mixin 都有的問題就是,兩者使用的是 靜態組合 而不是 動態組合。問問你本身:在 HOC 這個範式下,組合是在哪裏發生的?當組件類(如上例中的的 AppWithMouse)被建立後,發生了一次靜態組合。

你沒法在 render 方法中使用 mixin 或者 HOC,而這恰是 React 動態 組合模型的關鍵。當你在 render 中完成了組合,你就能夠利用到全部 React 生命期的優點了。動態組合或許微不足道,但興許某天也會出現一篇專門探討它的博客,等等,我有點離題了。😅

總而言之:使用 ES6 class 建立的 HOC 仍然會遇到和使用 createClass 時同樣的問題,它只能算一次重構。

如今不要說擁抱 HOC 了,咱們不過在擁抱新的 mixin!🤗

除了上述缺陷,因爲 HOC 的實質是包裹組件並建立了一個混入現有組件的 mixin 替代,所以,HOC 將引入大量的繁文縟節。從 HOC 中返回的組件須要表現得和它包裹的組件儘量同樣(它須要和包裹組件接收同樣的 props 等等)。這一事實使得構建健壯的 HOC 須要大量的樣板代碼(boilerplate code)。

上面我所講到的,以 React Router 中的 withRouter HOC 爲例,你能夠看到 props 傳遞wrappedComponentRef被包裹組件的靜態屬性提高(hoist)等等這樣的樣板代碼,當你須要爲你的 React 添加 HOC 時,就不得不撰寫它們。

Render Props

如今,有了另一門技術來作代碼複用,該技術能夠規避 mixin 和 HOC 的問題。在 React Training 中,稱之爲 「Render Props」。

我第一次見到 render prop 是在 ChengLou 在 React Europe 上 關於 react-motion 的演講,大會上,他提到的 <Motion children> API 能讓組件與它的父組件共享 interpolated animation。若是讓我來定義 render prop,我會這麼定義:

一個 render prop 是一個類型爲函數的 prop,它讓組件知道該渲染什麼。

更通俗的說法是:不一樣於經過 「混入」 或者裝飾來共享組件行爲,一個普通組件只須要一個函數 prop 就可以進行一些 state 共享

繼續到上面的例子,咱們將經過一個類型爲函數的 render 的 prop 來簡化 withMouse HOC 到一個普通的 <Mouse> 組件。而後,在 <Mouse>render 方法中,咱們可使用一個 render prop 來讓組件知道如何渲染:

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

// 與 HOC 不一樣,咱們可使用具備 render prop 的普通組件來共享代碼
class Mouse extends React.Component {
  static propTypes = {
    render: PropTypes.func.isRequired
  }

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

const App = React.createClass({
  render() {
    return (
      <div style={{ height: '100%' }}> <Mouse render={({ x, y }) => ( // render prop 給了咱們所須要的 state 來渲染咱們想要的 <h1>The mouse position is ({x}, {y})</h1> )}/> </div> ) } }) ReactDOM.render(<App/>, document.getElementById('app')) 複製代碼

這裏須要明確的概念是,<Mouse> 組件其實是調用了它的 render 方法來將它的 state 暴露給 <App> 組件。所以,<App> 能夠隨便按本身的想法使用這個 state,這太美妙了。😎

在此,我想說明,「children as a function」 是一個 徹底相同的概念,只是用 children prop 替代了 render prop。我掛在嘴邊的 render prop 並非在強調一個 名叫 prop 的 prop,而是在強調你使用一個 prop 去進行渲染的概念。

該技術規避了全部 mixin 和 HOC 會面對的問題:

  • ES6 class。不成問題,咱們能夠在 ES6 class 建立的組件中使用 render prop。
  • 不夠直接。咱們沒必要再擔憂 state 或者 props 來自哪裏。咱們能夠看到經過 render prop 的參數列表看到有哪些 state 或者 props 可供使用。
  • 名字衝突。如今不會有任何的自動屬性名稱合併,所以,名字衝突將全無可乘之機。

而且,render prop 也不會引入 任何繁文縟節,由於你不會 包裹裝飾 其餘的組件。它僅僅是一個函數!若是你使用了 TypeScript 或者 Flow,你會發現相較於 HOC,如今很容易爲你具備 render prop 的組件寫一個類型定義。固然,這是另一個話題了。

另外,這裏的組合模型是 動態的!每次組合都發生在 render 內部,所以,咱們就能利用到 React 生命週期以及天然流動的 props 和 state 帶來的優點。

使用這個模式,你能夠將 任何 HOC 替換一個具備 render prop 的通常組件。這點咱們能夠證實!😅

Render Props > HOCs

一個更將強有力的,可以證實 render prop 比 HOC 要強大的證據是,任何 HOC 都能使用 render prop 替代,反之則否則。下面的代碼展現了使用一個通常的、具備 render prop 的 <Mouse> 組件來實現的 withMouse HOC:

const withMouse = (Component) => {
  return class extends React.Component {
    render() {
      return <Mouse render={mouse => (
        <Component {...this.props} mouse={mouse}/>
      )}/>
    }
  }
}
複製代碼

有心的讀者可能已經意識到了 withRouter HOC 在 React Router 代碼庫中確實就是經過**一個 render prop ** 實現的!

因此還不心動?快去你本身的代碼中使用 render prop 吧!嘗試使用具備 render prop 組件來替換 HOC。當你這麼作了以後,你將再也不受困於 HOC 的繁文縟節,而且你也將利用到 React 給予的動態組合模型的好處,那是特別酷的特性。😎

MichaelReact Training 的成員,也是 React 社區中一個多產的開源軟件貢獻者。想了解最新的培訓和課程就[訂閱郵件推送](subscribe to the mailing list) 並 在 Twitter 上關注 React Training


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索