- 原文地址:Use a Render Prop!
- 原文做者:Michael Jackson
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:yoyoyohamapi
- 校對者:MechanicianW Usey95
更新:我提交了一個 PR 到 React 官方文檔,爲其添加了 Render props。html
更新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
若是你直接跳過視頻開始閱讀,但並無領會我所說的意思,就折回去看視頻吧。演講時的細節會更豐富。後端
個人演講始於高階組件主要解決的問題:代碼複用。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
屬性得到鼠標的 x
和 y
座標。
去年,隨着ES6 class 的到來,React 團隊最終決定使用 ES6 class 來代替 createClass
。這是一個明智的決定,沒有人會在 JavaScript 都內置了 class 時還會維護本身的類模型。
但就存在一個問題:ES6 class 不支持 mixin。除了不是 ES6 規範的一部分,Dan 已經在一篇 React 博客上發佈的博文上詳細討論了 mixin 存在的其餘問題。
minxins 的問題總結下來就是
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 時遇到的問題?
讓咱們看下:
另外一個 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 時,就不得不撰寫它們。
如今,有了另一門技術來作代碼複用,該技術能夠規避 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 會面對的問題:
而且,render prop 也不會引入 任何繁文縟節,由於你不會 包裹 和 裝飾 其餘的組件。它僅僅是一個函數!若是你使用了 TypeScript 或者 Flow,你會發現相較於 HOC,如今很容易爲你具備 render prop 的組件寫一個類型定義。固然,這是另一個話題了。
另外,這裏的組合模型是 動態的!每次組合都發生在 render 內部,所以,咱們就能利用到 React 生命週期以及天然流動的 props 和 state 帶來的優點。
使用這個模式,你能夠將 任何 HOC 替換一個具備 render prop 的通常組件。這點咱們能夠證實!😅
一個更將強有力的,可以證實 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 給予的動態組合模型的好處,那是特別酷的特性。😎
Michael 是 React Training 的成員,也是 React 社區中一個多產的開源軟件貢獻者。想了解最新的培訓和課程就[訂閱郵件推送](subscribe to the mailing list) 並 在 Twitter 上關注 React Training。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。