- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。