Utilisation des rappels dans React

Au cours de mon travail, je suis périodiquement tombé sur le fait que les développeurs ne comprennent pas toujours clairement comment le mécanisme de transmission des données via les accessoires, en particulier les rappels, et pourquoi leurs PureComponents sont mis à jour si souvent.


Par conséquent, dans cet article, nous comprendrons comment les rappels sont passés à React et discuterons également des fonctionnalités des gestionnaires d'événements.


TL; DR


  1. N'interférez pas avec JSX et la logique métier - cela compliquera la perception du code.
  2. Pour les petites optimisations, les fonctions du gestionnaire de cache sous la forme de classProperties pour les classes ou en utilisant useCallback pour les fonctions - alors les composants purs ne seront pas constamment rendus. En particulier, la mise en cache des rappels peut être utile afin que lorsqu'ils sont transmis au PureComponent, des cycles de mise à jour inutiles ne se produisent pas.
  3. N'oubliez pas que vous n'obtenez pas un événement réel dans le rappel, mais un événement Syntetic. Si vous quittez la fonction actuelle, vous ne pourrez pas accéder aux champs de cet événement. Mettez en cache les champs dont vous avez besoin si vous avez des fermetures asynchrones.

Partie 1. Gestionnaires d'événements, mise en cache et perception du code


React fournit un moyen assez pratique d'ajouter des gestionnaires d'événements pour les éléments html.


C'est l'une des choses fondamentales que tout développeur apprend lorsqu'il commence à écrire dans React:


class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } } 

Assez simple? À partir de ce code, il devient immédiatement clair ce qui se passera lorsque l'utilisateur cliquera sur le bouton.


Mais que se passe-t-il si le code dans le gestionnaire devient de plus en plus?


Supposons que par un bouton, nous devons charger et filtrer tous ceux qui ne font pas partie d'une équipe particulière ( user.team === 'search-team' ), puis les trier par âge.


 class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } } 

Ce code est assez difficile à comprendre. Le code de logique métier est mélangé avec la mise en page que l'utilisateur voit.


Le moyen le plus simple de s'en débarrasser est de prendre la fonction au niveau des méthodes de classe:


 class MyComponent extends Component { fetchUsers() { //    } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => this.fetchUsers()}>Load users</button> </div> ); } } 

Ici, nous avons déplacé la logique métier du code JSX vers un champ distinct de notre classe. Pour rendre cela accessible à l'intérieur de la fonction, nous avons défini le rappel de cette façon: onClick={() => this.fetchUsers()}


De plus, lors de la description d'une classe, nous pouvons déclarer un champ comme une fonction de flèche:


 class MyComponent extends Component { fetchUsers = () => { //    }; render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={this.fetchUsers}>Load users</button> </div> ); } } 

Cela nous permettra de déclarer le rappel comme onClick={this.fetchUsers}


Quelle est la différence entre ces deux méthodes?


onClick={this.fetchUsers} - Ici, à chaque appel à la fonction de rendu dans les accessoires, le button recevra toujours le même lien.


Dans le cas de onClick={() => this.fetchUsers()} , chaque fois que la fonction de rendu est appelée, JavaScript initialise une nouvelle fonction () => this.fetchUsers() et la définit sur prop onClick . Cela signifie que nextProp.onClick et prop.onClick sur le button dans ce cas ne seront toujours pas égaux, et même si le composant est marqué comme propre, il sera rendu.


Qu'est-ce que cela menace pour le développement?


Dans la plupart des cas, vous ne remarquerez pas de baisse des performances visuellement, car le DOM virtuel qui sera généré par le composant ne différera pas du précédent et il n'y aura aucun changement dans votre DOM.


Cependant, si vous affichez de grandes listes de composants ou de tableaux, vous remarquerez des "freins" sur une grande quantité de données.


Pourquoi est-il important de comprendre comment une fonction est transférée vers le rappel?


Souvent sur Twitter ou sur stackoverflow, vous pouvez trouver de tels conseils:


"Si vous rencontrez des problèmes de performances avec les applications React, essayez de remplacer l'héritage de Component par PureComponent. De plus, n'oubliez pas que pour Component, vous pouvez toujours définir shouldComponentUpdate pour éliminer les boucles de mise à jour inutiles."


Si nous définissons un composant comme Pure, cela signifie qu'il a déjà une fonction shouldComponentUpdate qui fait shallowEqual entre les accessoires et les nextProps.


En passant à chaque fois une nouvelle fonction de rappel à un tel composant, nous perdons tous les avantages et optimisations de PureComponent .


Regardons un exemple.
Créez un composant d'entrée qui affichera également des informations sur le nombre de fois qu'il a été mis à jour:


 class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } } 

Créons deux composants qui rendront l'entrée en interne:


 class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Et le second:


 class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Vous pouvez essayer l'exemple avec vos mains ici: https://codesandbox.io/s/2vwz6kjjkr
Cet exemple montre comment vous pouvez perdre tous les avantages de PureComponent si vous passez une nouvelle fonction de rappel au PureComponent à chaque fois.


Partie 2. Utilisation de gestionnaires d'événements dans les composants de fonction


Dans la nouvelle version de React (16.8), le mécanisme des crochets React a été annoncé, vous permettant d'écrire des composants fonctionnels à part entière, avec un cycle de vie clair qui peut couvrir presque tous les cas d'utilisateurs qui jusqu'à présent ne couvraient que les classes.


Nous modifions l'exemple avec le composant Input afin que tous les composants soient représentés par une fonction et travaillons avec React-hooks.


L'entrée doit stocker en elle-même des informations sur le nombre de fois où elle a été modifiée. Si dans le cas des classes, nous avons utilisé un champ dans notre instance, dont l'accès a été implémenté par ce biais, alors dans le cas d'une fonction, nous ne serons pas en mesure de déclarer une variable par ce biais.
React fournit un hook useRef qui peut être utilisé pour enregistrer une référence à l'élément HtmlElement dans l'arborescence DOM, mais il est également intéressant car il peut être utilisé pour les données régulières dont notre composant a besoin:


 import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); } 

Nous avons également besoin que le composant soit "propre", c'est-à-dire qu'il n'est mis à jour que si les accessoires qui ont été transmis au composant ont changé.
Pour cela, il existe différentes bibliothèques qui fournissent HOC, mais il est préférable d'utiliser la fonction mémo, qui est déjà intégrée à React, car elle fonctionne plus rapidement et plus efficacement:


 import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }); 

Le composant Input est prêt, maintenant nous réécrivons les composants A et B.
Dans le cas du composant B, cela est facile à faire:


 import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); } 

Ici, nous avons utilisé le hook useState , qui vous permet d'enregistrer et de travailler avec l'état du composant, au cas où le composant est représenté par une fonction.


Comment pouvons-nous mettre en cache la fonction de rappel? Nous ne pouvons pas le supprimer du composant, car dans ce cas, il sera commun à différentes instances du composant.
Pour de telles tâches, React dispose d'un ensemble de crochets de mise en cache et de mémorisation, dont useCallback est le plus approprié pour useCallback https://reactjs.org/docs/hooks-reference.html


Ajoutez ce crochet au composant A :


 import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); } 

Nous avons mis en cache la fonction, ce qui signifie que le composant Input ne sera pas mis à jour à chaque fois.


Comment fonctionne le crochet useCallback ?


Ce hook renvoie une fonction mise en cache (c'est-à-dire que le lien ne change pas de rendu en rendu).
En plus de la fonction à mettre en cache, un deuxième argument lui est transmis - un tableau vide.
Ce tableau vous permet de transférer une liste de champs, lors du changement dont vous avez besoin pour changer la fonction, c'est-à-dire retourner un nouveau lien.


useCallback pouvez voir la différence entre la méthode habituelle de transfert d'une fonction vers un rappel et useCallback ici: https://codesandbox.io/s/0y7wm3pp1w


Pourquoi avons-nous besoin d'un tableau?


Supposons que nous devons mettre en cache une fonction qui dépend d'une certaine valeur via une fermeture:


 import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ /*a*/ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement); 

Ici, le composant App dépend de prop a . Si vous exécutez l'exemple, alors tout fonctionnera correctement jusqu'au moment où nous ajouterons à la fin:


 setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000); 

Une fois le délai d'attente déclenché, lorsque vous cliquez sur le bouton en alerte, 1 s'affiche. Cela se produit car nous avons enregistré la fonction précédente, qui a fermé a variable. Et comme a est une variable, qui dans notre cas est un type de valeur, et que le type de valeur est immuable, nous avons eu cette erreur. Si nous supprimons le commentaire /*a*/ , le code fonctionnera correctement. Réagir sur le deuxième rendu vérifiera que les données passées dans le tableau sont différentes et renverra une nouvelle fonction.


Vous pouvez essayer cet exemple vous-même ici: https://codesandbox.io/s/6vo8jny1ln


React fournit de nombreuses fonctions qui vous permettent de mémoriser des données, telles que useRef , useCallback et useMemo .
Si ce dernier est nécessaire pour mémoriser la valeur de la fonction, et qu'ils useCallback assez similaires à useRef , alors useRef vous permet de mettre en cache non seulement les références aux éléments DOM, mais aussi d'agir comme un champ d'instance.


À première vue, il peut être utilisé pour mettre en cache des fonctions, car useRef met également en cache des données entre des mises à jour de composants distinctes.
Cependant, l'utilisation de useRef pour mettre en cache les fonctions n'est pas souhaitable. Si notre fonction utilise la fermeture, alors dans n'importe quel rendu, la valeur fermée peut changer et notre fonction mise en cache fonctionnera avec l'ancienne valeur. Cela signifie que nous devrons écrire la logique de mise à jour de la fonction ou simplement utiliser useCallback , dans laquelle elle est implémentée en raison du mécanisme de dépendance.


https://codesandbox.io/s/p70pprpvvx ici vous pouvez voir la mémorisation des fonctions avec le bon useCallback , avec le mauvais et avec useRef .


Partie 3. Événements syntétiques


Nous avons déjà compris comment utiliser les gestionnaires d'événements et comment fonctionner correctement avec les fermetures dans les rappels, mais dans React, il y a une autre différence très importante lorsque vous travaillez avec eux:


Remarque: maintenant Input , avec lequel nous avons travaillé ci-dessus, est absolument synchrone, mais dans certains cas, il peut être nécessaire que le rappel se produise avec un retard, selon le modèle de rebond ou de limitation . Ainsi, debounce, par exemple, est très pratique à utiliser pour la saisie de chaîne de recherche - la recherche ne se produit que lorsque l'utilisateur arrête de saisir des caractères.


Créez un composant qui provoque en interne un changement d'état:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Ce code ne fonctionnera pas. Le fait est que React proxie les événements à l'intérieur de lui-même, et le soi-disant événement syntétique entre dans notre rappel onChange, qui après notre fonction sera "effacé" (les champs seront nuls). Pour des raisons de performances, React fait cela pour utiliser un seul objet, plutôt que d'en créer un nouveau à chaque fois.


Si nous devons prendre de la valeur, comme dans cet exemple, alors il suffit de mettre en cache les champs nécessaires AVANT de quitter la fonction:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Vous pouvez voir un exemple ici: https://codesandbox.io/s/oj6p8opq0z


Dans de très rares cas, il devient nécessaire de maintenir l'instance entière de l'événement. Pour ce faire, vous pouvez appeler event.persist() , ce qui supprime
cette instance de l'événement Syntetic à partir du pool d'événements d'événements de réaction.


Conclusion:


Les gestionnaires d'événements React sont très pratiques car ils:


  1. Automatisez l'abonnement et le désabonnement (avec le composant de démontage);
  2. Simplifiez la perception du code, la plupart des abonnements sont faciles à suivre en code JSX.

Mais en même temps, lors du développement d'applications, vous pouvez rencontrer des difficultés:


  1. Remplacer les rappels dans les accessoires;
  2. Evénements syntaxiques effacés après l'exécution de la fonction actuelle.

Remplacer les rappels n'est généralement pas perceptible, car vDOM ne change pas, mais il convient de se rappeler que si vous introduisez des optimisations, en remplaçant les composants par Pure via l'héritage de PureComponent ou en utilisant memo , vous devez prendre soin de les mettre en cache, sinon les avantages de l'introduction de PureComponents ou memo ne seront pas perceptibles. Pour la mise en cache, vous pouvez utiliser classProperties (lorsque vous travaillez avec une classe) ou useCallback hook useCallback (lorsque vous travaillez avec des fonctions).


Pour un fonctionnement asynchrone correct, si vous avez besoin de données d'un événement, mettez également en cache les champs dont vous avez besoin.

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


All Articles