Automatisez la transition vers React Hooks

React 16.18 est la première version stable avec prise en charge des hooks React . Vous pouvez désormais utiliser des hooks sans craindre que l'API change radicalement. Bien que l' react développement de react conseille d'utiliser la nouvelle technologie uniquement pour les nouveaux composants, beaucoup, y compris moi-même, aimeraient les utiliser pour les composants plus anciens qui utilisent des classes. Mais comme le refactoring manuel est un processus laborieux, nous allons essayer de l'automatiser. Les techniques décrites dans cet article conviennent pour automatiser la refactorisation non seulement des composants react , mais également de tout autre code JavaScript .


Caractéristiques React Hooks


L'article React Hooks Introduction détaille ce que sont les hameçons et ce avec quoi ils mangent. En bref, il s'agit d'une nouvelle technologie folle pour créer state composants sans state sans utiliser de classes.


Considérez le fichier button.js :


 import React, {Component} from 'react'; export default Button; class Button extends Component { constructor() { super(); this.state = { enabled: true }; this.toogle = this._toggle.bind(this); } _toggle() { this.setState({ enabled: false, }); } render() { const {enabled} = this.state; return ( <button enabled={enabled} onClick={this.toggle} /> ); } } 

Avec des crochets, cela ressemblera à ceci:


 import React, {useState} from 'react'; export default Button; function Button(props) { const [enabled, setEnabled] = useState(true); function toggle() { setEnabled(false); } return ( <button enabled={enabled} onClick={toggle} /> ); } 

Vous pouvez discuter pendant longtemps de la façon dont ce type d'enregistrement est plus évident pour les personnes peu familiarisées avec la technologie, mais une chose est immédiatement claire: le code est plus concis et plus facile à réutiliser. Des ensembles intéressants de crochets personnalisés peuvent être trouvés sur usehooks.com et streamich.imtqy.com .


Ensuite, nous analyserons les différences de syntaxe dans les moindres détails et traiterons du processus de conversion de code de programme, mais avant cela, je voudrais parler d'exemples d'utilisation de cette forme de notation.


Digression lyrique: utilisation non standard de la syntaxe de déstructuration


ES2015 donné au monde une chose aussi merveilleuse que la restructuration des baies . Et maintenant, au lieu d'extraire chaque élément individuellement:


 const letters = ['a', 'b']; const first = letters[0]; const second = letters[1]; 

Nous pouvons obtenir tous les éléments nécessaires à la fois:


 const letters = ['a', 'b']; const [first, second] = letters; 

Un tel enregistrement est non seulement plus concis, mais aussi moins sujet aux erreurs, car il supprime la nécessité de se souvenir des indices des éléments et vous permet de vous concentrer sur ce qui est vraiment important: l'initialisation des variables.


Ainsi, nous arrivons à la es2015 que si ce n'était pas pour es2015 équipe de es2015 ne trouverait pas une manière aussi inhabituelle de travailler avec l'État.


Ensuite, je voudrais considérer plusieurs bibliothèques qui utilisent une approche similaire.


Essayez d'attraper


Six mois avant l'annonce de hooks dans la réaction, j'ai eu l'idée que la déstructuration peut être utilisée non seulement pour obtenir des données homogènes du tableau, mais aussi pour obtenir des informations sur une erreur ou le résultat d'une fonction, par analogie avec des rappels dans node.js. Par exemple, au lieu d'utiliser la syntaxe try-catch :


 let data; let error; try { data = JSON.parse('xxxx'); } catch (e) { error = e; } 

Ce qui semble très lourd, mais porte peu d'informations, et nous oblige à utiliser let , bien que nous n'ayons pas prévu de changer les valeurs des variables. Au lieu de cela, vous pouvez appeler la fonction try-catch , qui fera tout ce dont vous avez besoin, nous sauvant des problèmes répertoriés ci-dessus:


 const [error, data] = tryCatch(JSON.parse, 'xxxx'); 

De cette façon intéressante, nous nous sommes débarrassés de toutes les constructions syntaxiques inutiles, ne laissant que le nécessaire. Cette méthode présente les avantages suivants:


  • la possibilité de spécifier n'importe quel nom de variable qui nous convient (lors de l'utilisation de la déstructuration d'objets, nous n'aurions pas un tel privilège, ou plutôt, il aurait son propre prix encombrant);
  • la possibilité d'utiliser des constantes pour des données qui ne changent pas;
  • syntaxe plus concise, tout ce qui pourrait être supprimé est manquant;

Et, encore une fois, tout cela grâce à la syntaxe de la déstructuration des tableaux. Sans cette syntaxe, l'utilisation d'une bibliothèque serait ridicule:


 const result = tryCatch(JSON.parse, 'xxxx'); const error = result[0]; const data = result[1]; 

C'est toujours du code valide, mais il perd beaucoup par rapport à la déstructuration. Je veux également ajouter un exemple de la bibliothèque try-to-catch , avec l'avènement de async-await la construction try-catch est toujours pertinente et peut être écrite comme ceci:


 const [error, data] = await tryToCatch(readFile, path, 'utf8'); 

Si l'idée d'une telle utilisation de la déstructuration m'est venue, alors pourquoi pas les créateurs de la réaction aussi, car en fait, nous avons quelque chose comme une fonction qui a 2 valeurs de retour: un tuple d'un Haskell.


Sur cette digression lyrique, on peut achever et passer à la question de la transformation.


Conversion d'une classe dans React Hooks


Pour la conversion, nous utiliserons le transformateur putout AST, qui vous permet de modifier uniquement ce qui est nécessaire et le plugin @ putout / plugin-react-hooks .


Afin de convertir la classe héritée de Component en une fonction utilisant react-hooks , les étapes suivantes doivent être effectuées:


  • supprimer la bind
  • renommer les méthodes privées en public (supprimer "_");
  • changer this.state pour utiliser des crochets
  • changer this.setState pour utiliser des crochets
  • enlevez this de partout
  • convertir la class en fonction
  • dans les importations, utilisez useState au lieu de Component

Connexion


Installez putout avec le @putout/plugin-react-hooks :


 npm i putout @putout/plugin-react-hooks -D 

Ensuite, créez le fichier .putout.json :


 { "plugins": [ "react-hooks" ] } 

Essayez ensuite la putout en action.


En-tête de spoiler
 coderaiser@cloudcmd:~/example$ putout button.js /home/coderaiser/putout/packages/plugin-react-hooks/button.js 11:8 error bind should not be used react-hooks/remove-bind 14:4 error name of method "_toggle" should not start from under score react-hooks/rename-method-under-score 7:8 error hooks should be used instead of this.state react-hooks/convert-state-to-hooks 15:8 error hooks should be used instead of this.setState react-hooks/convert-state-to-hooks 21:14 error hooks should be used instead of this.state react-hooks/convert-state-to-hooks 7:8 error should be used "state" instead of "this.state" react-hooks/remove-this 11:8 error should be used "toogle" instead of "this.toogle" react-hooks/remove-this 11:22 error should be used "_toggle" instead of "this._toggle" react-hooks/remove-this 15:8 error should be used "setState" instead of "this.setState" react-hooks/remove-this 21:26 error should be used "state" instead of "this.state" react-hooks/remove-this 26:25 error should be used "setEnabled" instead of "this.setEnabled" react-hooks/remove-this 3:0 error class Button should be a function react-hooks/convert-class-to-function 12 errors in 1 files fixable with the `--fix` option 

putout trouvé 12 endroits qui peuvent être corrigés, essayez:


 putout --fix button.js 

Maintenant, button.js ressemble à ceci:


 import React, {useState} from 'react'; export default Button; function Button(props) { const [enabled, setEnabled] = useState(true); function toggle() { setEnabled(false); } return ( <button enabled={enabled} onClick={setEnabled} /> ); } 

Implémentation logicielle


Examinons plus en détail les différentes règles décrites ci-dessus.


Retirez this de partout


Puisque nous n'utilisons pas de classes, toutes les expressions de la forme this.setEnabled doivent être converties en setEnabled .


Pour ce faire, nous allons passer par les nœuds de ThisExpression , qui, à leur tour, sont des enfants de la relation à MemberExpression , et sont situés dans le champ object , ainsi:


 { "type": "MemberExpression", "object": { "type": "ThisExpression", }, "property": { "type": "Identifier", "name": "setEnabled" } } 

Considérez la mise en œuvre de la règle de suppression de cette règle:


 //      module.exports.report = ({name}) => `should be used "${name}" instead of "this.${name}"`; //    module.exports.fix = ({path}) => { // : MemberExpression -> Identifier path.replaceWith(path.get('property')); }; module.exports.find = (ast, {push}) => { traverseClass(ast, { ThisExpression(path) { const {parentPath} = path; const propertyPath = parentPath.get('property'); //      const {name} = propertyPath.node; push({ name, path: parentPath, }); }, }); }; 

Dans le code décrit ci-dessus, la fonction utilitaire traverseClass pour trouver la classe, elle n'est pas si importante pour une compréhension générale, mais il est toujours logique de l'apporter, pour une plus grande précision:


En-tête de spoiler
 //      function traverseClass(ast, visitor) { traverse(ast, { ClassDeclaration(path) { const {node} = path; const {superClass} = node; if (!isExtendComponent(superClass)) return; path.traverse(visitor); }, }); }; //       Component function isExtendComponent(superClass) { const name = 'Component'; if (isIdentifier(superClass, {name})) return true; if (isMemberExpression(superClass) && isIdentifier(superClass.property, {name})) return true; return false; } 

Le test, à son tour, peut ressembler à ceci:


 const test = require('@putout/test')(__dirname, { 'remove-this': require('.'), }); test('plugin-react-hooks: remove-this: report', (t) => { t.report('this', `should be used "submit" instead of "this.submit"`); t.end(); }); test('plugin-react-hooks: remove-this: transform', (t) => { const from = ` class Hello extends Component { render() { return ( <button onClick={this.setEnabled}/> ); } } `; const to = ` class Hello extends Component { render() { return <button onClick={setEnabled}/>; } } `; t.transformCode(from, to); t.end(); }); 

Dans les importations, utilisez useState au lieu de Component


Considérez l'implémentation de la règle convert-import-component-to-use-state .


Afin de remplacer les expressions:


 import React, {Component} from 'react' 

sur


 import React, {useState} from 'react' 

Vous devez traiter le nœud ImportDeclaration :


  { "type": "ImportDeclaration", "specifiers": [{ "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "React" } }, { "type": "ImportSpecifier", "imported": { "type": "Identifier", "name": "Component" }, "local": { "type": "Identifier", "name": "Component" } }], "source": { "type": "StringLiteral", "value": "react" } } 

Nous devons trouver ImportDeclaration avec source.value = react , puis faire le tour du tableau des specifiers à la recherche d' ImportSpecifier avec le champ name = Component :


 //     module.exports.report = () => 'useState should be used instead of Component'; //    module.exports.fix = (path) => { const {node} = path; node.imported.name = 'useState'; node.local.name = 'useState'; }; //    module.exports.find = (ast, {push, traverse}) => { traverse(ast, { ImportDeclaration(path) { const {source} = path.node; //   react,    if (source.value !== 'react') return; const name = 'Component'; const specifiersPaths = path.get('specifiers'); for (const specPath of specifiersPaths) { //    ImportSpecifier -    if (!specPath.isImportSpecifier()) continue; //    Compnent -    if (!specPath.get('imported').isIdentifier({name})) continue; push(specPath); } }, }); }; 

Considérez le test le plus simple:


 const test = require('@putout/test')(__dirname, { 'convert-import-component-to-use-state': require('.'), }); test('plugin-react-hooks: convert-import-component-to-use-state: report', (t) => { t.report('component', 'useState should be used instead of Component'); t.end(); }); test('plugin-react-hooks: convert-import-component-to-use-state: transform', (t) => { t.transformCode(`import {Component} from 'react'`, `import {useState} from 'react'`); t.end(); }); 

Et donc, nous avons examiné en termes généraux l'implémentation logicielle de plusieurs règles, les autres sont construites selon un schéma similaire. Vous pouvez vous familiariser avec tous les nœuds de l'arborescence du fichier analysé button.js dans astexplorer . Le code source des plugins décrits peut être trouvé dans le référentiel .


Conclusion


Aujourd'hui, nous avons examiné l'une des méthodes de refactorisation automatisée des classes de réactifs pour réagir. Actuellement, le @putout/plugin-react-hooks ne prend @putout/plugin-react-hooks charge que les mécanismes de base, mais il peut être considérablement amélioré si la communauté est intéressée et impliquée. Je serai heureux de discuter dans les commentaires des commentaires, des idées, des exemples d'utilisation, ainsi que les fonctionnalités manquantes.

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


All Articles