Automatice la transición a React Hooks

React 16.18 es la primera versión estable con soporte para ganchos de reacción . Ahora puede usar ganchos sin temor a que la API cambie drásticamente. Aunque el react desarrollo de react recomienda usar la nueva tecnología solo para nuevos componentes, muchos, incluido yo mismo, quisiéramos usarlos para componentes más antiguos que usan clases. Pero como la refactorización manual es un proceso laborioso, intentaremos automatizarlo. Las técnicas descritas en este artículo son adecuadas para automatizar la refactorización no solo de los componentes de react , sino también de cualquier otro código JavaScript .


Características Reaccionar ganchos


El artículo de Introducción a React Hooks detalla cuáles son los ganchos y con qué comen. En pocas palabras, esta es una nueva tecnología loca para crear componentes sin state sin usar clases.


Considere el archivo 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} /> ); } } 

Con ganchos, se verá así:


 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} /> ); } 

Puede discutir durante mucho tiempo cómo este tipo de grabación es más obvio para las personas que no están familiarizadas con la tecnología, pero una cosa está clara de inmediato: el código es más conciso y más fácil de reutilizar. Se pueden encontrar conjuntos interesantes de ganchos personalizados en usehooks.com y streamich.imtqy.com .


A continuación, analizaremos las diferencias de sintaxis hasta el más mínimo detalle y trataremos el proceso de conversión del código del programa, pero antes de eso me gustaría hablar sobre ejemplos de uso de esta forma de notación.


Digresión de letras: uso no estándar de sintaxis de desestructuración


ES2015 le dio al mundo algo tan maravilloso como la reestructuración de matrices . Y ahora, en lugar de extraer cada elemento individualmente:


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

Podemos obtener todos los elementos necesarios a la vez:


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

Tal registro no solo es más conciso, sino que también es menos propenso a errores, ya que elimina la necesidad de recordar índices de elementos y le permite concentrarse en lo que es realmente importante: la inicialización de variables.


Por lo tanto, llegamos a la es2015 que si no fuera por es2015 equipo de es2015 no llegaría a una forma tan inusual de trabajar con el estado.


A continuación, me gustaría considerar varias bibliotecas que utilizan un enfoque similar.


Intenta atrapar


Seis meses antes del anuncio de ganchos en la reacción, se me ocurrió la idea de que la desestructuración se puede usar no solo para obtener datos homogéneos de la matriz, sino también para obtener información sobre un error o el resultado de una función, por analogía con devoluciones de llamada en node.js. Por ejemplo, en lugar de usar la sintaxis try-catch :


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

Lo que parece muy engorroso, pero lleva poca información, y nos obliga a usar let , aunque no planeamos cambiar los valores de las variables. En cambio, puede llamar a la función try-catch , que hará todo lo que necesite, salvándonos de los problemas enumerados anteriormente:


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

De esta forma interesante, nos deshicimos de todas las construcciones sintácticas innecesarias, dejando solo lo necesario. Este método tiene las siguientes ventajas:


  • la capacidad de especificar cualquier nombre de variable que sea conveniente para nosotros (al usar la desestructuración de objetos, no tendríamos tal privilegio, o más bien, tendría su propio precio engorroso);
  • la capacidad de usar constantes para datos que no cambian;
  • sintaxis más concisa, falta todo lo que podría eliminarse;

Y, de nuevo, todo esto gracias a la sintaxis de la desestructuración de las matrices. Sin esta sintaxis, usar una biblioteca se vería ridículo:


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

Este sigue siendo un código válido, pero pierde significativamente en comparación con la desestructuración. También quiero agregar un ejemplo de la biblioteca try-to-catch , con el advenimiento de async-await la construcción try-catch sigue siendo relevante y puede escribirse así:


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

Si se me ocurrió la idea de tal uso de la desestructuración, ¿por qué no también los creadores de la reacción, porque de hecho, tenemos algo así como una función que tiene 2 valores de retorno: una tupla de un Haskell?


En esta digresión lírica se puede completar y pasar al tema de la transformación.


Convertir una clase en React Hooks


Para la conversión, utilizaremos el transformador AST de salida , que le permite cambiar solo lo que se necesita y el complemento @ putout / plugin-react-hooks .


Para convertir la clase heredada de Component en una función usando react-hooks , se deben seguir los siguientes pasos:


  • eliminar bind
  • cambiar el nombre de los métodos privados a public (eliminar "_");
  • cambie this.state para usar ganchos
  • cambie this.setState para usar ganchos
  • eliminar this de todas partes
  • convertir class para funcionar
  • en importaciones use useState lugar de Component

Conexión


Instale putout con el @putout/plugin-react-hooks :


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

A continuación, cree el archivo .putout.json :


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

Entonces intente putout en acción.


Spoiler header
 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 encontró 12 lugares que se pueden arreglar, intente:


 putout --fix button.js 

Ahora button.js ve así:


 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} /> ); } 

Implementación de software


Consideremos con más detalle varias de las reglas descritas anteriormente.


Eliminar this de todas partes


Como no usamos clases, todas las expresiones de la forma this.setEnabled deben convertirse a setEnabled .


Para hacer esto, pasaremos por los nodos de ThisExpression , que, a su vez, son hijos de la relación con MemberExpression y se encuentran en el campo de object , por lo tanto:


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

Considere la implementación de la regla remove-this :


 //      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, }); }, }); }; 

En el código descrito anteriormente, la función de utilidad traverseClass para encontrar la clase, no es tan importante para una comprensión general, pero aún tiene sentido traerla, para una mayor precisión:


Spoiler header
 //      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; } 

La prueba, a su vez, puede verse así:


 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(); }); 

En las importaciones, use useState lugar de Component


Considere la implementación de la regla convert-import-component-to-use-state .


Para reemplazar las expresiones:


 import React, {Component} from 'react' 

en


 import React, {useState} from 'react' 

Debe procesar el nodo 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" } } 

Necesitamos encontrar ImportDeclaration con source.value = react , y luego recorrer la matriz de specifiers en busca de ImportSpecifier con el campo 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); } }, }); }; 

Considere la prueba más 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(); }); 

Y así, examinamos en términos generales la implementación del software de varias reglas, el resto se construye de acuerdo con un esquema similar. Puede familiarizarse con todos los nodos del árbol del archivo analizado button.js en astexplorer . El código fuente de los complementos descritos se puede encontrar en el repositorio .


Conclusión


Hoy analizamos uno de los métodos para la refactorización automática de clases de ganchos de reacción a reacción. Actualmente, el @putout/plugin-react-hooks solo admite mecanismos básicos, pero se puede mejorar significativamente si la comunidad está interesada e involucrada. Estaré encantado de discutir en los comentarios comentarios, ideas, ejemplos de uso, así como la funcionalidad que falta.

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


All Articles