- 原文地址:React Higher-Order Components
- 原文做者:Tyler McGinnis
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:CoderMing
- 校對者:giddens9527、icy
在這篇文章的開始以前,咱們有兩點須要注意:首先,咱們所討論的僅僅是一種設計模式。它甚至就像組件結構同樣不是 React 裏的東西。第二,它不是構建一個 React 應用所必須的知識。你能夠關掉這篇文章、不學習在這篇文章中咱們所討論的內容,以後仍然能夠構建一個正常的 React 應用。不過,就像構建全部東西同樣,你有更多可用的工具就會獲得更好的結果。若是你在寫 React 應用,在你的「工具箱」之中沒有這個(React 高階組件)的話會對你是很是不利的。前端
在你聽到 Don't Repeat Yourself
或者 D.R.Y 這樣(中邪同樣)的口號以前你是不會在軟件開發的鑽研之路上走得很遠的。有時候實行這些名言會有點過於麻煩,可是在大多數狀況下,(實行它)是一個有價值的目標。在這篇文章中咱們將會去探討在 React 庫中實現 DRY 的最著名的模式——高階組件。不過在咱們探索答案以前,咱們首先必需要徹底明確問題來源。react
假設咱們要負責從新建立一個相似於 Sprite(譯者注:國外的一個在線支付公司)的儀表盤。正如大多數項目那樣,一切事務在最後收尾以前都工做得很正常。你發如今儀表盤上有一串不同的提示框須要你某些元素 hover 的時候顯示。 => 你在儀表盤上面發現了一些不一樣的、(當鼠標)懸停在某些組成元素上面會出現的提示信息。android
這裏有好幾種方式能夠實現這個效果。其中一個你可能想到的是監聽特定的組件的 hover 狀態來決定是否展現 tooltip。在上圖中,你有三個組件須要添加它們的監聽功能—— Info
、TrendChart
和 DailyChart
。ios
讓咱們從 Info
組件開始。如今它只是一個簡單的 SVG 圖標。git
class Info extends React.Component {
render() {
return (
<svg
className="Icon-svg Icon--hoverable-svg"
height={this.props.height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
)
}
}
複製代碼
如今咱們須要添加讓它能夠監測到自身是否被(鼠標)懸停的功能。咱們可使用 React 所附帶的 onMouseOver
和 onMouseOut
這兩個鼠標時間。咱們傳遞給 onMouseOver
的函數將會在組件被鼠標懸停後觸發,同時咱們傳遞給 onMouseOut
的函數將會在組件再也不被鼠標懸停時觸發。要以 React 的方式來操做,咱們會給給咱們的組件添加一個 hovering
state 屬性,因此咱們能夠在 hovering
state 屬性改變的時候觸發重繪,來展現或者隱藏咱們的提示框。github
class Info extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<>
{this.state.hovering === true
? <Tooltip id={this.props.id} />
: null}
<svg
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
className="Icon-svg Icon--hoverable-svg"
height={this.props.height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
</>
)
}
}
複製代碼
上面的代碼看起來很棒。如今咱們要添加一樣的功能給咱們的其餘兩個組件——TrendChart
和 DailyChart
。若是這兩個組件沒有出問題,就請不要修復它。咱們對於 Info
的懸停功能運行的很好,因此請再寫一遍以前的代碼。編程
class TrendChart extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<>
{this.state.hovering === true
? <Tooltip id={this.props.id}/>
: null}
<Chart
type='trend'
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
/>
</>
)
}
}
複製代碼
你或許知道下一步了:咱們要對最後一個組件 DailyChart
作一樣的事情。後端
class DailyChart extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<>
{this.state.hovering === true
? <Tooltip id={this.props.id}/>
: null}
<Chart
type='daily'
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
/>
</>
)
}
}
複製代碼
這樣的話,咱們就所有作完了。你可能之前曾經這樣寫過 React 代碼。但這並不應是你最終所該作的(不過這樣作也還湊合),可是它很不 「DRY」。正如咱們所看到的,咱們在咱們的每個組件中都 重複着徹底同樣的的鼠標懸停邏輯。設計模式
從這點看的話,問題變得很是清晰了:咱們但願避免在在每一個須要添加鼠標懸停邏輯的組件是都再寫一遍相同的邏輯。因此,解決辦法是什麼?在咱們開始前,讓咱們先討論一些能讓咱們更容易理解答案的編程思想—— 回調函數
和 高階函數
。數組
在 JavaScript 中,函數是 「一等公民」。這意味着它就像對象/數組/字符串那樣能夠被聲明爲一個變量、看成函數的參數或者在函數中返回一個函數,即便返回的是其餘函數也能夠。
function add (x, y) {
return x + y
}
function addFive (x, addReference) {
return addReference(x, 5)
}
addFive(10, add) // 15
複製代碼
若是你沒這樣用過,你可能會感到困惑。咱們將 add
函數做爲一個參數傳入 addFive
函數,從新命名爲 addReference
,而後咱們調用了着個函數。
這時候,你做爲參數所傳遞進去的函數被叫作回調函數同時你使用回調函數所構建的新函數被叫作高階函數。
由於這些名詞很重要,下面是一份根據它們所表示的含義從新命名變量後的一樣邏輯的代碼。
function add (x,y) {
return x + y
}
function higherOrderFunction (x, callback) {
return callback(x, 5)
}
higherOrderFunction(10, add)
複製代碼
這個模式很常見,哪裏都有它。若是你以前用過任何 JavaScript 數組方法、jQuery 或者是 lodash 這類的庫,你就已經用太高階函數和回調函數了。
[1,2,3].map((i) => i + 5)
_.filter([1,2,3,4], (n) => n % 2 === 0 );
$('#btn').on('click', () =>
console.log('回調函數哪裏都有')
)
複製代碼
讓咱們回到咱們以前的例子。若是咱們不只僅想建立一個 addFive
函數,咱們也想建立 addTen
函數、 addTwenty
函數等等,咱們該怎麼辦?在咱們當前的實踐方法中,咱們必須在須要的時候去重複地寫咱們的邏輯。
function add (x, y) {
return x + y
}
function addFive (x, addReference) {
return addReference(x, 5)
}
function addTen (x, addReference) {
return addReference(x, 10)
}
function addTwenty (x, addReference) {
return addReference(x, 20)
}
addFive(10, add) // 15
addTen(10, add) // 20
addTwenty(10, add) // 30
複製代碼
再一次出現這種狀況,這樣寫並不糟糕,可是咱們重複寫了好多類似的邏輯。這裏咱們的目標是要能根據須要寫不少 「adder」 函數(addFive
、addTen
、addTwenty
等等),同時儘量減小代碼重複。爲了完成這個目標,咱們建立一個 makeAdder
函數怎麼樣?着個函數能夠傳入一個數字和原始 add
函數。由於這個函數的目的是建立一個新的 adder 函數,咱們可讓其返回一個全新的傳遞數字來實現加法的函數。這兒講的有點多,讓咱們來看下代碼吧。
function add (x, y) {
return x + y
}
function makeAdder (x, addReference) {
return function (y) {
return addReference(x, y)
}
}
const addFive = makeAdder(5, add)
const addTen = makeAdder(10, add)
const addTwenty = makeAdder(20, add)
addFive(10) // 15
addTen(10) // 20
addTwenty(10) // 30
複製代碼
太酷了!如今咱們能夠在須要的時候隨意地用最低的代碼重複度建立 「adder」 函數。
若是你在乎的話,這個經過一個多參數的函數來返回一個具備較少參數的函數的模式被叫作 「部分應用(Partial Application)「,它也是函數式編程的技術。JavaScript 內置的 「.bind「 方法也是一個相似的例子。
好吧,那這與 React 以及咱們以前遇到鼠標懸停的組件有什麼關係呢?咱們剛剛經過建立了咱們的 makeAdder
這個高階函數來實現了代碼複用,那咱們也能夠建立一個相似的 「高階組件」 來幫助咱們實現相同的功能(代碼複用)。不過,不像高階函數返回一個新的函數那樣,高階組件返回一個新的組件來渲染 「回調」 組件🤯。這裏有點複雜,讓咱們來攻克它。
function higherOrderFunction (callback) {
return function () {
return callback()
}
}
複製代碼
function higherOrderComponent (Component) {
return class extends React.Component {
render() {
return <Component />
}
}
}
複製代碼
咱們已經有了一個高階函數的基本概念了,如今讓咱們來完善它。若是你還記得的話,咱們以前的問題是咱們重複地在每一個須要的組件上寫咱們的鼠標懸停的處理邏輯。
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
複製代碼
考慮到這一點,咱們但願咱們的高階組件(咱們把它稱做 withHover
)自身須要能封裝咱們的鼠標懸停處理邏輯而後傳遞 hovering
state 給其所須要渲染的組件。這將容許咱們可以複用鼠標懸停邏輯,並將其裝入單一的位置(withHover
)。
最後,下面的代碼就是咱們的最終目標。不管何時咱們想讓一個組件具備 hovering
state,咱們均可以經過將它傳遞給withHover 高階組件來實現。
const InfoWithHover = withHover(Info)
const TrendChartWithHover = withHover(TrendChart)
const DailyChartWithHover = withHover(DailyChart)
複製代碼
因而,不管給 withHover
傳遞什麼組件,它都會渲染原始組件,同時傳遞一個 hovering
prop。
function Info ({ hovering, height }) {
return (
<>
{hovering === true
? <Tooltip id={this.props.id} />
: null}
<svg
className="Icon-svg Icon--hoverable-svg"
height={height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
</>
)
}
複製代碼
如今咱們須要作的最後一件事是實現 withHover
。正如咱們上面所看到的:
function withHover (Component) {
}
複製代碼
function withHover (Component) {
return class WithHover extends React.Component {
}
}
複製代碼
如今問題變爲了咱們應該如何獲取 hovering
呢?好吧,咱們已經有以前寫邏輯的代碼了。咱們僅僅須要將其添加到一個新的組件同時將 hovering
state 做爲一個 prop 傳遞給參數中的 組件
。
function withHover(Component) {
return class WithHover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
<Component hovering={this.state.hovering} />
</div>
);
}
}
}
複製代碼
我比較喜歡的思考這些知識的方式(同時也在 React 文檔中有提到)是 **組件是將 props 轉化到視圖層,高階組件則是將一個組件轉化到另外一個組件。**在咱們的例子中,咱們將咱們的 Info
、TrendChart
和 DailyChart
組件搬運到一個具備 hovering
prop 的組件中。
至此,咱們已經涵蓋到了高階組件的全部基礎知識。這裏還有一些很重要的知識咱們須要來講明下。
若是你再回去看咱們的 withHover
高階組件的話,它有一個缺點就是它已經假定了一個名爲 hovering
的 prop。在大多數狀況下這樣或許是沒問題的,可是在某些狀況下會出問題。舉個例子,若是(原來的)組件已經有一個叫作 hovering
的 prop 呢?這裏咱們出現了命名衝突。咱們能夠作的是讓咱們的 withHover
高階組件可以容許用戶本身定義傳入子組件的 prop 名。由於 withHover
只是一個函數,讓咱們讓它的第二個參數來描述傳遞給子組件 prop 的名字。
function withHover(Component, propName = 'hovering') {
return class WithHover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
const props = {
[propName]: this.state.hovering
}
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
<Component {...props} />
</div>
);
}
}
}
複製代碼
如今咱們設置了默認的 prop 名稱爲 hovering
(經過使用 ES6 的默認參數特性來實現),若是用戶想改變 withHover
的默認 prop 名的話,能夠經過第二個參數來傳遞一個新的 prop 名。
function withHover(Component, propName = 'hovering') {
return class WithHover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
const props = {
[propName]: this.state.hovering
}
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
<Component {...props} />
</div>
);
}
}
}
function Info ({ showTooltip, height }) {
return (
<>
{showTooltip === true
? <Tooltip id={this.props.id} />
: null}
<svg
className="Icon-svg Icon--hoverable-svg"
height={height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
</>
)
}
const InfoWithHover = withHover(Info, 'showTooltip')
複製代碼
你可能發現了咱們的 withHover
函數實現的另一個問題。看看咱們的 Info
組件,·你可能會發現其還有一個 height
屬性,可是 height
將會是 undefined。其緣由是咱們的 withHover
組件是渲染 Component
組件的函數。事實上咱們這樣作的話,除了 hovering
prop 之外咱們不會傳遞任何 prop 給咱們最終建立的 <Component />
。
const InfoWithHover = withHover(Info)
...
return <InfoWithHover height="16px" />
複製代碼
height
prop 經過 InfoWithHover
組件傳入,可是這個組件是從哪兒來的?它是咱們經過 withHover
所建立並返回的那個組件。
function withHover(Component, propName = 'hovering') {
return class WithHover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
console.log(this.props) // { height: "16px" }
const props = {
[propName]: this.state.hovering
}
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
<Component {...props} />
</div>
);
}
}
}
複製代碼
深刻 WithHover
組件內部,this.props.height
的值是 16px
可是咱們沒有用它作任何事情。咱們須要確保咱們將其傳入給咱們實際渲染的 Component
。
render() {
const props = {
[propName]: this.state.hovering,
...this.props,
}
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
<Component {...props} />
</div>
);
}
複製代碼
由此來看,咱們已經感覺到了使用高階組件減小代碼重複的諸多優勢。可是,它(高階組件)還有什麼坑嗎?固然有,咱們立刻就去踩踩這些坑。
當咱們使用高階組件時,會發生一些 控制反轉 的狀況。想象下咱們正在用相似於 React Router 的 withRouter
這類第三方的高階組件。 根據它們的文檔,「withRouter
將會在任何被它包裹的組件渲染時,將 match
、location
和 history
prop 傳遞給它們。
class Game extends React.Component {
render() {
const { match, location, history } = this.props // From React Router
...
}
}
export default withRouter(Game)
複製代碼
請注意,咱們並無(由 <Game />
組件直接)在界面上渲染 Game
元素。咱們將咱們的組件全權交給了 React Router 同時咱們也相信其不止能正確渲染組件,也能正確傳遞 props。咱們以前在討論 hovering
prop 命名衝突的時候看到過這個問題。爲了修復這個問題咱們嘗試着給咱們的 withHover
高階組件傳遞第二個參數來容許修改 prop 的名字。可是在使用第三方高階組件的時候,咱們沒有這個配置項。若是咱們的 Game
組件已經使用了 match
、location
或者 history
的話,就沒有(像使用咱們本身的組件)那沒幸運了。咱們除了改變咱們以前所須要使用的 props 名以外就只能不使用 withRouter
高階組件了。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。