Composants d'ordre supérieur dans React

Récemment, nous avons publié du matériel sur les fonctions d'ordre supérieur en JavaScript destiné à ceux qui apprennent JavaScript. L'article que nous traduisons aujourd'hui est destiné aux développeurs débutants de React. Il se concentre sur les composants d'ordre supérieur (HOC).



Principe DRY et composants d'ordre supérieur dans React


Vous ne pourrez pas avancer assez loin dans l’étude de la programmation et vous ne rencontrerez pas le principe presque culte de DRY (ne vous répétez pas, ne répétez pas). Parfois, ses partisans vont même trop loin, mais, dans la plupart des cas, cela vaut la peine de s'efforcer de se conformer. Ici, nous allons parler du modèle de développement React le plus populaire, qui garantit la conformité avec le principe DRY. Il s'agit de composants d'ordre supérieur. Afin de comprendre la valeur des composants d'ordre supérieur, formulons et comprenons d'abord le problème qu'ils sont censés résoudre.

Supposons que vous deviez recréer un panneau de contrôle similaire au panneau Stripe. De nombreux projets ont la propriété de se développer selon le schéma, quand tout va bien jusqu'au moment où le projet est terminé. Lorsque vous pensez que le travail est presque terminé, vous remarquez que le panneau de configuration comporte de nombreuses info-bulles différentes qui devraient apparaître lorsque vous survolez certains éléments.


Panneau de configuration et info-bulles

Afin de mettre en œuvre une telle fonctionnalité, vous pouvez utiliser plusieurs approches. Vous avez décidé de le faire: déterminez si le pointeur est au-dessus d'un composant individuel, puis décidez d'afficher ou non un indice pour celui-ci. Trois composants doivent être équipés de fonctionnalités similaires. Ce sont Info , TrendChart et DailyChart .

Commençons par le composant Info . À l'heure actuelle, il s'agit d'une simple icône 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>   ) } } 

Maintenant, nous devons rendre ce composant capable de déterminer si le pointeur de la souris est au-dessus ou non. Vous pouvez utiliser les événements de souris onMouseOver et onMouseOut pour cela. La fonction passée à onMouseOver sera appelée si le pointeur de la souris est tombé dans la zone du composant, et la fonction passée à onMouseOut sera appelée lorsque le pointeur quitte le composant. Afin d'organiser tout cela d'une manière acceptée dans React, nous ajoutons la propriété de hovering au composant, qui est stockée dans l'état, ce qui nous permet de restituer le composant en affichant ou en masquant l'info-bulle si cette propriété change.

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

Cela s'est plutôt bien passé. Nous devons maintenant ajouter la même fonctionnalité à deux autres composants - TrendChart et DailyChart . Le mécanisme ci-dessus pour le composant Info fonctionne bien, ce qui n'est pas cassé n'a pas besoin d'être réparé, alors recréons le même dans d'autres composants en utilisant le même code. Recyclez le code du composant 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}       />     </>   ) } } 

Vous avez probablement déjà compris quoi faire ensuite. La même chose peut être faite avec notre dernier composant - 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}       />     </>   ) } } 

Maintenant, tout est prêt. Vous avez peut-être déjà écrit quelque chose de similaire sur React. Bien sûr, ce n'est pas le pire code au monde, mais il ne suit pas particulièrement bien le principe DRY. Comme vous pouvez le constater, en analysant le code du composant, nous répétons dans chacun d'eux la même logique.

Le problème auquel nous sommes confrontés devrait maintenant devenir extrêmement clair. Il s'agit d'un code en double. Pour le résoudre, nous voulons nous débarrasser de la nécessité de copier le même code dans les cas où ce que nous avons déjà implémenté est nécessaire à un nouveau composant. Comment le résoudre? Avant d'en parler, nous nous concentrerons sur plusieurs concepts de programmation qui faciliteront grandement la compréhension de la solution proposée ici. Nous parlons de rappels et de fonctions d'ordre supérieur.

Fonctions d'ordre supérieur


Les fonctions en JavaScript sont des objets de première classe. Cela signifie qu'ils peuvent, comme les objets, les tableaux ou les chaînes, être affectés à des variables, passés à des fonctions en tant qu'arguments ou renvoyés par d'autres fonctions.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } addFive(10, add) // 15 

Si vous n'êtes pas habitué à ce comportement, le code ci-dessus peut vous sembler étrange. Parlons de ce qui se passe ici. À savoir, nous transmettons la fonction add à la fonction addFive en tant qu'argument, la addReference en addReference , puis l'appelons.

Lorsque de telles constructions sont utilisées, une fonction passée en argument à un autre est appelée rappel (fonction de rappel), et une fonction qui reçoit une autre fonction en argument est appelée fonction d'ordre supérieur.

Nommer les entités en programmation est important, voici donc le même code utilisé dans lequel les noms sont modifiés conformément aux concepts qu'ils représentent.

 function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add) 

Ce modèle devrait vous sembler familier. Le fait est que si vous avez utilisé, par exemple, des méthodes de tableau JavaScript, travaillé avec jQuery ou lodash, vous avez déjà utilisé des fonctions et des rappels d'ordre supérieur.

 [1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') ) 

Revenons à notre exemple. Et si, au lieu de simplement créer la fonction addFive , nous voulions créer la fonction addTwenty , addTwenty et d'autres comme ça. Étant donné la façon dont la fonction addFive est addFive , nous devrons copier son code et le changer pour créer les fonctions susmentionnées en fonction de celui-ci.

 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 

Il convient de noter que notre code n'était pas si cauchemardesque, mais il est clair que de nombreux fragments y sont répétés. Notre objectif est que nous puissions créer autant de fonctions qui ajoutent certains nombres aux nombres qui leur sont addFive ( addFive , addTen , addTwenty , etc.) autant que nous en avons besoin, tout en minimisant la duplication de code. Peut-être que pour y parvenir, nous devons créer une fonction makeAdder ? Cette fonction peut prendre un certain nombre et un lien vers la fonction d' add . Puisque le but de cette fonction est de créer une nouvelle fonction qui ajoute le numéro qui lui est transmis à la donnée, nous pouvons faire en sorte que la fonction makeAdder retourne une nouvelle fonction qui contient un certain nombre (comme le numéro 5 dans makeFive ) et qui pourrait prendre des nombres à ajouter à ce nombre.

Jetez un œil à un exemple de mise en œuvre des mécanismes ci-dessus.

 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 

Nous pouvons maintenant créer autant de fonctions add que nécessaire, tout en minimisant la quantité de duplication de code.

Si cela est intéressant, le concept selon lequel il existe une certaine fonction qui traite d'autres fonctions afin qu'elles puissent être utilisées avec moins de paramètres qu'auparavant est appelé «application partielle de la fonction». Cette approche est utilisée en programmation fonctionnelle. Un exemple de son utilisation est la méthode .bind utilisée en JavaScript.

Tout cela est bien, mais qu'est-ce que React a à voir avec le problème ci-dessus de duplication du code pour le traitement des événements de souris lors de la création de nouveaux composants qui ont besoin de cette fonctionnalité? Le fait est que, tout comme la fonction d'ordre supérieur makeAdder nous aide à minimiser la duplication de code, ce que l'on appelle le «composant d'ordre supérieur» nous aidera à résoudre le même problème dans une application React. Cependant, ici, tout sera un peu différent. À savoir, au lieu d'un schéma de travail, au cours duquel une fonction d'ordre supérieur renvoie une nouvelle fonction qui appelle un rappel, un composant d'ordre supérieur peut implémenter son propre schéma. À savoir, il est capable de renvoyer un nouveau composant qui rend un composant qui joue le rôle de «rappel». Nous avons peut-être déjà dit beaucoup de choses, il est donc temps de passer à des exemples.

Notre fonction d'ordre le plus élevé


Cette fonctionnalité présente les fonctionnalités suivantes:

  • C'est une fonction.
  • Elle accepte, comme argument, un rappel.
  • Il renvoie une nouvelle fonction.
  • La fonction qu'elle renvoie peut appeler le rappel d'origine qui a été transmis à notre fonction d'ordre supérieur.

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

Notre composant de premier ordre


Ce composant peut être caractérisé comme suit:

  • C'est un composant.
  • Il prend comme argument un autre composant.
  • Il renvoie un nouveau composant.
  • Le composant qu'il renvoie peut rendre le composant d'origine transmis au composant d'ordre supérieur.

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

Mise en œuvre du HOC


Maintenant que nous avons, en termes généraux, compris exactement quelles actions le composant d'ordre supérieur effectue, nous allons commencer à apporter des modifications à notre code React. Si vous vous en souvenez, l'essence du problème que nous résolvons est que le code qui implémente la logique de traitement des événements de souris doit être copié sur tous les composants qui ont besoin de cette fonctionnalité.

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

Compte tenu de cela, nous avons besoin de notre composant d'ordre supérieur (appelons-le withHover ) pour encapsuler le code de traitement des événements de souris, puis passer la propriété de withHover aux composants qu'il rend. Cela nous permettra d'éviter la duplication du code correspondant en le plaçant dans le composant withHover .

En fin de compte, c'est ce que nous voulons atteindre. Chaque fois que nous avons besoin d'un composant qui doit avoir une idée de sa propriété de hovering , nous pouvons passer ce composant à un composant d'ordre supérieur avec withHover . Autrement dit, nous voulons travailler avec des composants comme indiqué ci-dessous.

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

Ensuite, lorsque le résultat de withHover sera rendu, ce sera le composant source auquel la propriété de hovering est transmise.

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

En fait, il ne nous reste plus qu'à implémenter le composant withHover . De ce qui précède, on peut comprendre qu'il doit effectuer trois actions:

  • Prenez un argument pour Component.
  • Renvoie un nouveau composant.
  • Rendez l'argument Component en lui passant la propriété hovering .

▍ Accepter l'argument Component


 function withHover (Component) { } 

▍Retour d'un nouveau composant


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

▍ Rendu du composant Component avec la propriété hovering qui lui est transmise


Maintenant, nous sommes confrontés à la question suivante: comment se rendre à la propriété en hovering ? En fait, nous avons déjà écrit le code pour travailler avec cette propriété. Il nous suffit de l'ajouter au nouveau composant, puis de lui passer la propriété de hovering lors du rendu du composant transmis au composant d'ordre supérieur sous la forme de l'argument Component .

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

Je préfère parler de ces choses de la manière suivante (comme le dit la documentation de React): le composant transforme les propriétés en interface utilisateur et le composant d'ordre supérieur transforme le composant en un autre composant. Dans notre cas, nous allons transformer les composants Info , TrendChart et DailyChart en nouveaux composants qui, grâce à la propriété de DailyChart , savent si le pointeur de la souris est au-dessus d'eux.

Notes supplémentaires


À ce stade, nous avons examiné toutes les informations de base sur les composants d'ordre supérieur. Cependant, il y a des choses plus importantes à discuter.

Si vous jetez un œil à notre HOC withHover , vous remarquerez qu'il a au moins un point faible. Cela implique que le composant récepteur de la propriété en hovering ne rencontrera aucun problème avec cette propriété. Dans la plupart des cas, cette hypothèse est susceptible d'être justifiée, mais il peut arriver qu'elle soit inacceptable. Par exemple, que faire si un composant possède déjà une propriété de hovering ? Dans ce cas, il y aura une collision de noms. Par conséquent, une withHover peut être apportée au composant withHover , qui permet à l'utilisateur de ce composant de spécifier le nom que la propriété de hovering transmise aux composants doit porter. Étant donné que withHover n'est qu'une fonction, withHover -la afin qu'elle withHover deuxième argument qui définit le nom de la propriété à transmettre au composant.

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

Maintenant, grâce au mécanisme de paramètre par défaut ES6, nous définissons la valeur standard du deuxième argument comme étant en hovering , mais si l'utilisateur du composant withHover veut changer cela, il peut passer, dans ce deuxième argument, le nom dont il a besoin.

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

Problème avec la mise en œuvre de Hover


Vous avez peut-être remarqué un autre problème avec l'implémentation de withHover . Si nous analysons notre composant Info , vous remarquerez qu'il accepte, entre autres, la propriété height . La façon dont nous avons tout organisé maintenant signifie que la height sera définie sur undefined . La raison en est que le composant withHover est le composant responsable du rendu de ce qui lui est transmis en tant qu'argument Component . Maintenant, nous ne transférons aucune propriété autre que le hovering nous avons créé au composant Component .

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

La propriété height est transmise au composant InfoWithHover . Et quel est ce composant? C'est le composant dont nous withHover avec 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>     );   } } } 

À l'intérieur du composant WithHover this.props.height est de this.props.height , mais à l'avenir, nous ne ferons rien avec cette propriété. Nous devons faire passer cette propriété à l'argument Component , que nous rendons.

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

À propos des problèmes liés à l'utilisation de composants tiers de premier ordre


Nous pensons que vous avez déjà apprécié les avantages de l'utilisation de composants d'ordre supérieur dans la réutilisation de la logique dans différents composants sans avoir à copier le même code. Maintenant, demandons-nous s'il y a des défauts dans les composants d'ordre supérieur. Cette question peut recevoir une réponse positive et nous avons déjà rencontré ces lacunes.

Lorsque vous utilisez HOC, l' inversion de contrôle se produit. Imaginez que nous utilisons un composant d'ordre supérieur qui n'a pas été développé par nous, comme le HOC withRouter React Router. Selon la documentation, withRouter transmettra les propriétés de match , d' location et d' history au composant qu'il a encapsulé lors du rendu.

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

Veuillez noter que nous ne créons pas d'élément de Game (c'est-à-dire - <Game /> ). Nous transférons entièrement notre composant React Router et faisons confiance à ce composant non seulement pour le rendu, mais également pour transmettre les propriétés correctes à notre composant. Nous avons déjà rencontré ce problème avant lorsque nous avons parlé d'un éventuel conflit de noms lors du passage de la propriété en hovering . Afin de résoudre ce problème, nous avons décidé d'autoriser l' withHover HOC withHover utiliser le deuxième argument pour configurer le nom de la propriété correspondante. En utilisant le HOC de quelqu'un d'autre avec le withRouter nous n'avons pas une telle opportunité. Si les propriétés de match , d' location ou d' history sont déjà utilisées dans le composant Game , alors nous pouvons dire que nous n'avons pas eu de chance. À savoir, nous devons soit modifier ces noms dans notre composant, soit refuser d'utiliser HOC avec withRouter .

Résumé


En parlant de HOC dans React, il y a deux choses importantes à garder à l'esprit. Tout d'abord, HOC n'est qu'un modèle. Les composants d'ordre supérieur ne peuvent même pas être appelés quelque chose de spécifique à React, malgré le fait qu'ils soient liés à l'architecture de l'application. Deuxièmement, pour développer des applications React, vous n'avez pas besoin de connaître les composants de niveau supérieur. Vous ne les connaissez peut-être pas bien, mais écrivez d'excellents programmes. Cependant, comme dans toute entreprise, plus vous disposez d'outils, meilleur est le résultat de votre travail. Et, si vous écrivez des applications à l'aide de React, vous vous rendrez service sans ajouter de HOC à votre arsenal.

Chers lecteurs! Utilisez-vous des composants d'ordre supérieur dans React?

Source: https://habr.com/ru/post/fr428572/


All Articles