React中的高阶组件

最近,我们针对学习JavaScript的人发布了JavaScript高阶函数的资料 。 我们今天翻译的文章是为初学者React开发人员准备的。 它着重于高阶组件(HOC)。



React中的DRY原理和高阶组件


您将无法在编程研究中取得足够的进步,也不会遇到DRY几乎崇高的原则(不要重复自己,不要重复)。 有时他的追随者走得太远,但是,在大多数情况下,值得争取遵守。 在这里,我们将讨论最流行的React开发模式,该模式可确保遵守DRY原则。 它是关于高阶组件。 为了了解高阶组件的价值,让我们首先阐述和理解它们打算解决的问题。

假设您需要重新创建类似于“条纹”面板的控制面板。 当一切都很好直到项目完成的那一刻,许多项目都有根据计划开发的属性。 当您认为工作即将完成时,您会注意到当将鼠标悬停在某些元素上时,控制面板上会出现许多不同的工具提示。


控制面板和工具提示

为了实现这种功能,可以使用几种方法。 您决定执行以下操作:确定指针是否在单个组件之上,然后决定是否为其显示提示。 需要为三个组件配备相似的功能。 这些是InfoTrendChartDailyChart

让我们从Info组件开始。 现在,它是一个简单的SVG图标。

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

现在,我们需要使该组件能够确定鼠标指针是否位于其上方。 您可以为此使用onMouseOveronMouseOut鼠标事件。 如果鼠标指针落入组件区域,则传递给onMouseOut的函数将被调用;传递给onMouseOut的函数将在指针离开组件时被调用。 为了以React可接受的方式组织所有这些工作,我们向组件中添加了hovering属性,该属性存储在状态中,这允许我们通过在属性改变时显示或隐藏工具提示来重新渲染组件。

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

结果很好。 现在,我们需要将相同的功能添加到另外两个组件DailyChartDailyChart 。 上面有关Info组件的机制运行良好,没有损坏的部分不需要修复,因此让我们使用相同的代码在其他组件中重新创建相同的组件。 回收TrendChart组件的代码。

 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('Callbacks are everywhere') ) 

让我们回到我们的例子。 如果不是要创建addFive函数,而是要创建addTen函数, addTwenty以及其他类似的方法,该addTen addTwenty ? 给定addFive函数的addFive ,我们将不得不复制其代码并对其进行更改以基于该代码创建上述函数。

 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 

应当指出,我们的代码并不是那么噩梦,但是很显然,其中的许多片段都是重复的。 我们的目标是创建尽可能多的函数,以在需要的地方添加一定数量的数字(如addFiveaddTenaddTwenty等),同时最大程度地减少代码重复。 也许要实现这一点,我们需要创建一个makeAdder函数? 该函数可以使用一定数量,并且可以带有指向add函数的链接。 由于此函数的目的是创建一个新函数,该函数将传递给它的数字添加到给定的数字,因此我们可以使makeAdder函数返回一个包含某个数字(例如makeFive的数字5)的新函数,该函数可以采用数字添加到该号码。

看一看上述机制的实现示例。

 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 

现在,我们可以根据需要创建任意数量的add功能,同时最大程度地减少代码重复量。

如果有意思的话,就是说某个功能可以处理其他功能,以便可以使用比以前更少的参数的概念称为“功能的部分应用”。 此方法用于函数式编程。 其用法的一个示例是JavaScript中使用的.bind方法。

所有这些都很好,但是在创建需要此功能的新组件时,React与上面的代码重复处理鼠标事件的处理有什么关系? 事实是,就像高阶函数makeAdder可以帮助我们最大程度地减少代码重复一样,所谓的“高阶组件”也可以帮助我们处理React应用程序中的相同问题。 但是,这里的一切看起来都会有所不同。 即,代替工作方案(在此方案中,高阶函数返回调用回调的新函数),高阶组件可以实现自己的方案。 即,他能够返回一个新的组件,该组件将呈现一个充当“回调”角色的组件。 也许我们已经说了很多话,所以现在该继续进行示例了。

我们最高阶的功能


此功能具有以下功能:

  • 她是一个功能。
  • 她接受回调作为参数。
  • 它返回一个新函数。
  • 它返回的函数可以调用传递给我们的高阶函数的原始回调。

 function higherOrderFunction (callback) { return function () {   return callback() } } 

我们最高阶的组件


该组件的特征如下:

  • 它是一个组件。
  • 作为参数,它包含另一个组件。
  • 它返回一个新的组件。
  • 它返回的组件可以使原始组件传递给高阶组件。

 function higherOrderComponent (Component) { return class extends React.Component {   render() {     return <Component />   } } } 

HOC实施


现在,我们已经大致确定了高阶组件执行的操作,我们将开始对React代码进行更改。 如果您还记得的话,我们要解决的问题的实质是必须将实现处理鼠标事件的逻辑的代码复制到需要此功能的所有组件中。

 state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) 

鉴于此,我们需要高阶组件(用withHover来称呼它)来封装鼠标事件处理代码,然后将withHover属性传递给它呈现的组件。 这将允许我们通过将其放置在withHover组件中来防止相应代码的重复。

最终,这是我们要实现的目标。 每当我们需要一个需要了解其hovering属性的组件时,都可以使用withHover将该组件传递给更高阶的组件。 也就是说,我们要使用如下所示的组件。

 const InfoWithHover = withHover(Info) const TrendChartWithHover = withHover(TrendChart) const DailyChartWithHover = withHover(DailyChart) 

然后,当withHover内容呈现时,它将成为传递hovering属性的源组件。

 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组件。 从以上所述可以理解,他必须执行三个动作:

  • 给Component一个参数。
  • 返回一个新组件。
  • 通过将hovering属性传递给Component参数来呈现它。

▍接受Component参数


 function withHover (Component) { } 

▍返回新组件


 function withHover (Component) { return class WithHover extends React.Component { } } 

with使用传递的悬停属性渲染Component组件


现在,我们面临以下问题:如何进入hovering属性? 实际上,我们已经编写了使用此属性的代码。 我们只需要将其添加到新组件中,然后在将组件以Component参数的形式传递给高阶组件时,将hovering属性传递给它。

 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文档所说):组件将属性转换为用户界面,而高阶组件将组件转换为另一个组件。 在我们的例子中,我们将把InfoTrendChartDailyChart组件转换为新的组件,这些组件由于具有DailyChart属性而知道鼠标指针是否在其上方。

附加说明


至此,我们已经回顾了有关高阶组件的所有基本信息。 但是,还有一些更重要的事情要讨论。

如果您查看我们的HOC withHover ,您会发现它至少有一个弱点。 这意味着hovering属性的接收器组件将不会遇到此属性的任何问题。 在大多数情况下,这种假设可能是合理的,但是可能会发生这种情况是不可接受的。 例如,如果组件已经具有hovering属性该怎么办? 在这种情况下,将发生名称冲突。 因此,可以对withHover组件进行withHover ,以允许该组件的用户指定传递给组件的hovering属性应使用的名称。 由于withHover只是一个函数,让我们对其进行重写,以便它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() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

现在,由于使用了ES6默认参数机制,我们将第二个参数的标准值设置为withHover ,但是如果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() {     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参数传递给它的内容的Component 。 现在,除了将我们创建的hovering传递给Component组件之外,我们不再传递任何其他属性。

 const InfoWithHover = withHover(Info) ... return <InfoWithHover height="16px" /> 

height属性将传递到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组件内部WithHover this.props.height16px ,但是将来我们将对该属性不做任何事情。 我们需要将该属性传递给正在渲染的Component参数。

 render() {     const props = {       [propName]: this.state.hovering,       ...this.props,     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     ); } 

关于使用最高级别的第三方组件的问题


我们相信,您已经意识到在复制各种组件中的逻辑时使用高级组件的优势,而无需复制相同的代码。 现在让我们自问,高阶组件是否存在任何缺陷。 这个问题可以得到肯定的回答,我们已经克服了这些缺点。

使用HOC时,会发生控制反转 。 想象一下,我们使用的不是我们开发的高阶组件,例如HOC withRouter React Router。 根据文档, withRouter会将matchlocationhistory属性传递给它在渲染时包装的组件。

 class Game extends React.Component { render() {   const { match, location, history } = this.props // From React Router   ... } } export default withRouter(Game) 

请注意,我们没有创建Game元素(即- <Game /> )。 我们完全转移了我们的React Router组件,并不仅信任该组件进行渲染,而且还将正确的属性传递给我们的组件。 在讨论传递hovering属性时可能发生的名称冲突之前,我们已经遇到了此问题。 为了解决此问题,我们决定允许HOC withHover使用第二个参数来配置相应属性的名称。 在withRouter使用其他人的HOC,我们没有这样的机会。 如果matchlocationhistory属性已在Game组件中使用,则可以说我们不走运。 即,我们要么在组件中更改这些名称,要么拒绝将HOC与withRouter一起使用。

总结


谈到React中的HOC,有两点需要牢记。 首先,HOC只是一种模式。 尽管高阶组件与应用程序的体系结构有关,但它们甚至都不能称为React特有的组件。 其次,要开发React应用程序,您不需要了解高阶组件。 您可能不熟悉它们,但是编写了出色的程序。 但是,就像在任何企业中一样,您拥有的工具越多,您的工作成果就越好。 而且,如果您使用React编写应用程序,则会给自己造成损害,而不会在您的武器库中添加HOC。

亲爱的读者们! 您在React中使用高阶组件吗?

Source: https://habr.com/ru/post/zh-CN428572/


All Articles