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-bullesAfin 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)
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)
À 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
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?
