En quoi les composants React fonctionnels sont-ils différents des composants basés sur les classes?

En quoi les composants React fonctionnels sont-ils différents des composants basés sur les classes? Depuis un certain temps maintenant, la réponse traditionnelle à cette question est: "L'utilisation de classes vous permet d'utiliser un grand nombre de fonctionnalités de composants, par exemple l'état." Maintenant, avec l'avènement des crochets , cette réponse ne reflète plus la véritable situation.

Vous avez peut-être entendu dire que l'un de ces types de composants a de meilleures performances que l'autre. Mais lequel? La plupart des repères qui testent cela ont des défauts , donc je tirerais des conclusions basées sur leurs résultats avec une grande prudence. Les performances dépendent principalement de ce qui se passe dans le code, et non de la sélection des composants fonctionnels ou des composants basés sur les classes pour implémenter certaines fonctionnalités. Notre étude a montré que la différence de performance entre les différents types de composants est négligeable. Cependant, il convient de noter que les stratégies d'optimisation utilisées pour travailler avec elles diffèrent légèrement.



En tout cas, je ne recommande pas de réécrire les composants existants à l'aide de nouvelles technologies s'il n'y a pas de bonnes raisons à cela, et si cela ne vous dérange pas de faire partie de ceux qui ont commencé à utiliser ces technologies avant tout le monde. Les crochets sont encore une nouvelle technologie (identique à la bibliothèque React en 2014), et certaines «meilleures pratiques» pour leur application n'ont pas encore été incluses dans les manuels React.

Où en sommes-nous finalement arrivés? Existe-t-il des différences fondamentales entre les composants fonctionnels de React et les composants basés sur les classes? Bien sûr, il existe de telles différences. Ce sont des différences dans le modèle mental d'utilisation de ces composants. Dans cet article, je considérerai leur différence la plus sérieuse. Il existe depuis que, en 2015, des composants fonctionnels sont apparus, mais il est souvent négligé. Elle consiste dans le fait que les composants fonctionnels capturent les valeurs rendues. Parlons de ce que cela signifie vraiment.

Il convient de noter que ce matériel ne constitue pas une tentative d'évaluation de composants de différents types. Je viens de décrire la différence entre les deux modèles de programmation dans React. Si vous souhaitez en savoir plus sur l'utilisation des composants fonctionnels à la lumière des innovations, reportez-vous à cette liste de questions et réponses sur les crochets.

Quelles sont les caractéristiques du code des composants basés sur des fonctions et des classes?


Considérez ce composant:

function ProfilePage(props) {  const showMessage = () => {    alert('Followed ' + props.user);  };  const handleClick = () => {    setTimeout(showMessage, 3000);  };  return (    <button onClick={handleClick}>Follow</button>  ); } 

Il affiche un bouton qui, en appuyant sur la fonction setTimeout , imite une demande réseau, puis affiche une boîte de message confirmant que l'opération est terminée. Par exemple, si « props.user 'Dan' est stocké dans props.user , alors dans la fenêtre de message, après trois secondes, 'Followed Dan' s'affiche.

Notez que peu importe si des fonctions fléchées ou des déclarations de fonctions sont utilisées ici. Une construction de la function handleClick() formulaire function handleClick() fonctionnera exactement de la même manière.

Comment réécrire ce composant en classe? Si vous venez de refaire le code que vous venez d'examiner, en le convertissant en code d'un composant basé sur une classe, vous obtenez ce qui suit:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; handleClick = () => {   setTimeout(this.showMessage, 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Il est généralement admis que deux de ces fragments de code sont équivalents. Et les développeurs sont souvent totalement libres, au cours du refactoring de code, se transforment les uns en les autres, sans penser aux conséquences possibles.


Ces morceaux de code semblent être équivalents

Cependant, il existe une légère différence entre ces extraits de code. Regardez-les de plus près. Tu vois la différence? Par exemple, je ne l'ai pas vue tout de suite.

De plus, nous considérerons cette différence, par conséquent, pour ceux qui veulent comprendre l'essence de ce qui se passe eux-mêmes, un exemple de travail de ce code.

Avant de continuer, je voudrais souligner que la différence en question n'a rien à voir avec les crochets React. Dans les exemples précédents, d'ailleurs, les crochets ne sont même pas utilisés. Il s'agit de la différence entre les fonctions et les classes dans React. Et si vous prévoyez d'utiliser de nombreux composants fonctionnels dans vos applications React, vous souhaiterez peut-être comprendre cette différence.

En fait, nous illustrerons la différence entre les fonctions et les classes par l'exemple d'une erreur qui est souvent rencontrée dans les applications React.

L'erreur courante dans les applications React.


Ouvrez la page d'exemple qui affiche une liste qui vous permet de sélectionner des profils utilisateur et deux boutons Follow qui sont affichés par les ProfilePageClass ProfilePageFunction et ProfilePageClass , fonctionnels et basés sur la classe, dont le code est illustré ci-dessus.

Essayez, pour chacun de ces boutons, d'effectuer la séquence d'actions suivante:

  1. Cliquez sur le bouton.
  2. Modifiez le profil sélectionné avant 3 secondes après avoir cliqué sur le bouton.
  3. Lisez le texte affiché dans la boîte de message.

Cela fait, vous remarquerez les fonctionnalités suivantes:

  • Lorsque vous cliquez sur le bouton formé par le composant fonctionnel avec le profil Dan sélectionné, puis que vous passez au profil Sophie , 'Followed Dan' s'affiche dans la boîte de message.
  • Si vous faites de même avec un bouton formé par un composant basé sur une classe, 'Followed Sophie' s'affichera.


Caractéristiques des composants basés sur les classes

Dans cet exemple, le comportement du composant fonctionnel est correct. Si je me suis abonné au profil de quelqu'un, puis suis passé à un autre profil, mon composant ne devrait pas douter du profil auquel je me suis abonné. De toute évidence, la mise en œuvre du mécanisme en question basé sur l'utilisation de classes contient une erreur (à propos, vous devriez certainement devenir abonné à Sofia ).

Causes de dysfonctionnement d'un composant basé sur une classe


Pourquoi un composant basé sur une classe se comporte-t-il de cette façon? Afin de comprendre cela, jetons un coup d'œil à la méthode showMessage dans notre classe:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; 

Cette méthode lit les données de this.props.user . Les propriétés de React sont immuables, elles ne changent donc pas. Cependant, comme toujours, il s'agit d'une entité mutable.

En fait, le but d'avoir this dans une classe réside dans la capacité de this à changer. La bibliothèque React elle-même effectue périodiquement this mutations, ce qui permet de travailler avec les dernières versions de la méthode de render et des méthodes de cycle de vie des composants.

Par conséquent, si notre composant effectue un nouveau rendu pendant l'exécution de la demande, this.props changera. Après cela, la méthode showMessage lira la valeur user de l'entité d' props "trop ​​nouveau".

Cela vous permet de faire une observation intéressante concernant les interfaces utilisateur. Si nous disons que l'interface utilisateur, conceptuellement, est fonction de l'état actuel de l'application, alors les gestionnaires d'événements font partie des résultats de rendu - tout comme les résultats de rendu visibles. Nos gestionnaires d'événements «appartiennent» à une opération de rendu spécifique avec des propriétés et un état spécifiques.

Cependant, la planification d'un délai dont le rappel this.props lu par this.props viole cette connexion. Le showMessage showMessage showMessage pas «lié» à une opération de rendu particulière, par conséquent, il «perd» les propriétés correctes. La lecture des données à partir de this rompt cette connexion.

Comment, au moyen de composants basés sur les classes, résoudre le problème?


Imaginez qu'il n'y ait pas de composants fonctionnels dans React. Comment alors résoudre ce problème?

Nous avons besoin d'un mécanisme pour "restaurer" la connexion entre la méthode de render avec les propriétés correctes et le showMessage showMessage, qui lit les données des propriétés. Ce mécanisme devrait être situé quelque part où l'essence des props avec les données correctes est perdue.

Une façon de procéder consiste à lire this.props à l'avance dans le gestionnaire d'événements, puis à transmettre explicitement ce qui a été lu à la fonction de rappel utilisée dans setTimeout :

 class ProfilePage extends React.Component { showMessage = (user) => {   alert('Followed ' + user); }; handleClick = () => {   const {user} = this.props;   setTimeout(() => this.showMessage(user), 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Cette approche fonctionne . Mais les constructions supplémentaires utilisées ici, au fil du temps, entraîneront une augmentation du volume du code et le fait que la probabilité d'erreurs y augmentera. Et si nous avons besoin de plus qu'une seule propriété? Et si nous devons également travailler avec l'État? Si la méthode showMessage une autre méthode et que cette méthode lit this.props.something ou this.state.something , nous rencontrerons à nouveau le même problème. Et pour le résoudre, nous devons passer this.props et this.state comme arguments à toutes les méthodes appelées depuis showMessage .

Si cela est vrai, cela détruira toutes les commodités offertes par l’utilisation des composants basés sur les classes. Il est difficile de se rappeler que travailler avec des méthodes de cette manière est difficile, il est difficile d'automatiser, en conséquence, les développeurs conviennent souvent, au lieu d'utiliser des méthodes similaires, qu'il y a des erreurs dans leurs projets.

De même, l'incorporation de code d' alert dans handleClick ne résout pas un problème plus global. Nous devons structurer le code afin qu'il puisse être divisé en plusieurs méthodes, mais aussi pour pouvoir lire les propriétés et l'état qui correspondent à l'opération de rendu associée à un appel particulier. Ce problème, d'ailleurs, ne s'applique même pas exclusivement à React. Vous pouvez le lire dans n'importe quelle bibliothèque pour développer des interfaces utilisateur, ce qui place les données dans des objets mutables comme this .

Peut-être que pour résoudre ce problème, vous pouvez lier des méthodes à this dans le constructeur?

 class ProfilePage extends React.Component { constructor(props) {   super(props);   this.showMessage = this.showMessage.bind(this);   this.handleClick = this.handleClick.bind(this); } showMessage() {   alert('Followed ' + this.props.user); } handleClick() {   setTimeout(this.showMessage, 3000); } render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Mais cela ne résout pas notre problème. N'oubliez pas que c'est que nous lisons les données de this.props trop tard, et non dans la syntaxe utilisée! Cependant, ce problème sera résolu si nous nous appuyons sur les fermetures JavaScript.

Les développeurs essaient souvent d'éviter les fermetures, car il n'est pas facile de penser à des valeurs qui, au fil du temps, ne peuvent pas muter. Mais les propriétés de React sont immuables! (Ou, au minimum, cela est fortement recommandé). Cela vous permet d'arrêter de percevoir les fermetures comme quelque chose à cause duquel le programmeur peut, comme on dit, «se tirer une balle dans le pied».

Cela signifie que si vous «verrouillez» les propriétés ou l'état d'une opération de rendu particulière dans la fermeture, vous pouvez toujours compter sur eux pour ne pas changer.

 class ProfilePage extends React.Component { render() {   //  !   const props = this.props;   //    ,      render.   //   -   .   const showMessage = () => {     alert('Followed ' + props.user);   };   const handleClick = () => {     setTimeout(showMessage, 3000);   };   return <button onClick={handleClick}>Follow</button>; } } 

Comme vous pouvez le voir, nous avons ici «capturé» les propriétés lors de l'appel à la méthode de render .


Propriétés capturées par l'appel de rendu

Avec cette approche, tout code trouvé dans la méthode de render (y compris showMessage ) est garanti de voir les propriétés capturées lors d'un appel particulier à cette méthode. En conséquence, React ne pourra plus nous empêcher de faire ce dont nous avons besoin.

Dans la méthode de render , vous pouvez décrire autant de fonctions auxiliaires que vous le souhaitez et toutes pourront utiliser les propriétés et l'état «capturés». C'est ainsi que les fermetures ont résolu notre problème.

Analyse de la solution du problème à l'aide de la fermeture


Ce que nous venons d'arriver nous permet de résoudre le problème , mais un tel code semble étrange. Pourquoi une classe est-elle nécessaire du tout si des fonctions sont déclarées à l'intérieur de la méthode de render , et non en tant que méthodes de classe?

En fait, nous pouvons simplifier ce code en nous débarrassant du «shell» sous la forme d'une classe qui l'entoure:

 function ProfilePage(props) { const showMessage = () => {   alert('Followed ' + props.user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Ici, comme dans l'exemple précédent, les propriétés sont capturées dans la fonction, puisque React leur les transmet en argument. Contrairement à this , React ne props jamais props objet d' props .

Cela devient un peu plus évident si les props détruits dans la déclaration de fonction:

 function ProfilePage({ user }) { const showMessage = () => {   alert('Followed ' + user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Lorsque le composant parent ProfilePage avec d'autres propriétés, React appellera à ProfilePage fonction ProfilePage . Mais le gestionnaire d'événements qui a déjà été appelé appartient à l'appel précédent à cette fonction, cet appel utilise sa propre valeur user et son propre showMessage showMessage, qui lit cette valeur. Tout cela reste intact.

C'est pourquoi dans la version originale de notre exemple, lorsque vous travaillez avec un composant fonctionnel, la sélection d'un autre profil après avoir cliqué sur le bouton correspondant avant que le message ne s'affiche ne change rien. Si un profil Sophie été sélectionné avant de cliquer sur le bouton, 'Followed Sophie' s'affichera dans la fenêtre de message, quoi qu'il arrive.


Utiliser un composant fonctionnel

Ce comportement est correct (vous pouvez également vous inscrire à Sunil en passant ).

Nous avons maintenant compris quelle est la grande différence entre les fonctions et les classes dans React. Comme déjà mentionné, nous parlons du fait que les composants fonctionnels capturent des valeurs. Parlons maintenant des crochets.

Crochets


Lors de l'utilisation de hooks, le principe de "capture de valeurs" s'étend à l'état. Prenons l'exemple suivant:

 function MessageThread() { const [message, setMessage] = useState(''); const showMessage = () => {   alert('You said: ' + message); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value); }; return (   <>     <input value={message} onChange={handleMessageChange} />     <button onClick={handleSendClick}>Send</button>   </> ); } 

Ici, vous pouvez expérimenter avec lui

Bien qu'il ne s'agisse pas d'un exemple exemplaire d'une interface d'application de messagerie, ce projet illustre la même idée: si un utilisateur envoie un message, le composant ne doit pas être confondu sur le message envoyé. La constante de message de ce composant fonctionnel capture l'état qui "appartient" au composant qui rend le navigateur le gestionnaire de clics pour le bouton qu'il appelle. Par conséquent, le message stocke ce qui était dans le champ de saisie au moment où vous cliquez sur le bouton Send .

Le problème de la capture des propriétés et des états par des composants fonctionnels


Nous savons que les composants fonctionnels de React capturent par défaut les propriétés et l'état. Mais que se passe-t-il si nous devons lire les dernières données des propriétés ou des états qui n'appartiennent pas à un appel de fonction particulier? Et si nous voulons «les lire du futur »?

Dans les composants basés sur les classes, cela pourrait être fait simplement en se référant à this.props ou this.state , car this s'agit d'une entité mutable. Son changement est engagé dans React. Les composants fonctionnels peuvent également fonctionner avec des valeurs mutables partagées par tous les composants. Ces valeurs sont appelées ref :

 function MyComponent() { const ref = useRef(null); //     `ref.current`. // ... } 

Cependant, le programmeur doit gérer ces valeurs indépendamment.

L'essence de ref joue le même rôle que les champs d'une instance d'une classe. Il s'agit d'une «sortie d'urgence» dans un monde impératif mutable. Vous connaissez peut-être le concept des références DOM, mais cette idée est beaucoup plus générale. Il peut être comparé à une boîte dans laquelle un programmeur peut mettre quelque chose.

Même à l'extérieur, une construction comme this.something ressemble à une image miroir de something.current construction actuelle. Ils sont une représentation du même concept.

Par défaut, React ne crée pas d'entités de ref dans les composants fonctionnels pour les valeurs de propriété ou d'état les plus récentes. Dans de nombreux cas, vous n'en aurez pas besoin et leur création automatique serait une perte de temps. Cependant, travailler avec eux, si nécessaire, peut être organisé de manière autonome:

 function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value);   latestMessage.current = e.target.value; }; 

Si nous lisons le message dans showMessage , nous verrons alors le message qui se trouvait dans le champ au moment de cliquer sur le bouton Send . Mais si vous lisez latestMessage.current , vous pouvez obtenir la dernière valeur - même si nous continuons à saisir du texte dans le champ après avoir cliqué sur le bouton Send .

Vous pouvez comparer ceci et ces exemples afin d'évaluer indépendamment la différence. La valeur de ref est un moyen «d'éviter» l'uniformité du rendu, dans certains cas, elle peut être très utile.

En général, vous devez éviter de lire ou d'écrire des valeurs ref pendant le processus de rendu car ces valeurs sont mutables. Nous nous efforçons de rendre le rendu prévisible. Cependant, si nous devons obtenir la valeur la plus récente de quelque chose stocké dans des propriétés ou dans un état, la mise à jour manuelle de la valeur ref peut être une tâche fastidieuse. Il peut être automatisé en utilisant l'effet:

 function MessageThread() { const [message, setMessage] = useState(''); //    . const latestMessage = useRef(''); useEffect(() => {   latestMessage.current = message; }); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; 

Voici un exemple qui utilise ce code

Nous attribuons une valeur à l'intérieur de l'effet, par conséquent, la valeur de ref ne changera qu'après la mise à jour du DOM. Cela garantit que notre mutation ne perturbe pas des fonctionnalités comme Time Slicing et Suspense , qui reposent sur la continuité des opérations de rendu.

L'utilisation de la valeur ref de cette manière n'est pas souvent requise. La capture de propriétés ou d'états semble généralement être un modèle bien meilleur de comportement standard du système. Cependant, cela peut être pratique lorsque vous travaillez avec des API impératives , comme celles qui utilisent des intervalles ou des abonnements. N'oubliez pas que vous pouvez travailler de cette façon avec n'importe quelle valeur - avec des propriétés, avec des variables stockées dans l'état, avec l'objet props entier props ou même avec une fonction.

Ce modèle peut en outre être utile à des fins d'optimisation. Par exemple, lorsque quelque chose comme useCallback change trop souvent. Certes, la solution préférée est souvent d' utiliser un réducteur .

Résumé


Dans cet article, nous avons examiné l'un des mauvais modèles d'utilisation des composants basés sur les classes et expliqué comment résoudre ce problème avec les fermetures. Cependant, vous pouvez remarquer que lorsque vous essayez d'optimiser les hooks en spécifiant un tableau de dépendances, vous pouvez rencontrer des erreurs liées à des fermetures obsolètes. Est-ce à dire que les défauts eux-mêmes sont un problème. Je ne pense pas.

Comme indiqué ci-dessus, les fermetures nous aident en fait à résoudre les petits problèmes difficiles à détecter. De même, ils facilitent l'écriture de code qui fonctionne correctement en parallèle . Cela est possible du fait que dans le composant, les propriétés et l'état corrects avec lesquels ce composant a été rendu sont «verrouillés».

Dans tous les cas que j'ai vus jusqu'à présent, le problème des «fermetures obsolètes» s'est produit en raison de l'hypothèse erronée que «les fonctions ne changent pas» ou que «les propriétés restent toujours les mêmes». J'espère qu'après avoir lu ce document, vous êtes convaincu que ce n'est pas le cas.

Les fonctions «capturent» leurs propriétés et leur état - et il est donc également important de comprendre quelles fonctions sont en question. Ce n'est pas une erreur, c'est une caractéristique des composants fonctionnels. Les fonctions ne doivent pas être exclues du "tableau de dépendances" pour useEffect ou useCalback , par exemple. (Un outil approprié pour résoudre le problème est généralement useReducer ou useRef . Nous en avons parlé ci-dessus, et bientôt nous préparerons du matériel qui sera consacré au choix de telle ou telle approche).

Si la plupart du code de nos applications sera basé sur des composants fonctionnels, cela signifie que nous devons en savoir plus sur l' optimisation du code et quelles valeurs peuvent changer au fil du temps.

: « , , , , , ».

. , React , . , « », . , React .

, , .


React —

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


All Articles