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 équivalentsCependant, 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:
- Cliquez sur le bouton.
- Modifiez le profil sélectionné avant 3 secondes après avoir cliqué sur le bouton.
- 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 classesDans 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() {
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 renduAvec 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 fonctionnelCe 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);
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('');
→
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 —