Automatize a transição para React Hooks

O React 16.18 é a primeira versão estável com suporte para ganchos de reação . Agora você pode usar ganchos sem medo de que a API mude drasticamente. Embora a react desenvolvimento de react recomende o uso da nova tecnologia apenas para novos componentes, muitos, inclusive eu, gostariam de usá-los para componentes mais antigos que usam classes. Mas como a refatoração manual é um processo trabalhoso, tentaremos automatizá-la. As técnicas descritas neste artigo são adequadas para automatizar a refatoração não apenas de componentes de react , mas também de qualquer outro código JavaScript .


Recursos Ganchos de reação


O artigo React Hooks Introduction detalha o que são os ganchos e o que eles comem. Em poucas palavras, esta é uma nova tecnologia maluca para criar componentes livres de state sem usar classes.


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

Com ganchos, ficará assim:


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

Você pode argumentar por um longo tempo como esse tipo de gravação é mais óbvio para pessoas não familiarizadas com a tecnologia, mas uma coisa é imediatamente clara: o código é mais conciso e mais fácil de reutilizar. Conjuntos interessantes de ganchos personalizados podem ser encontrados em usehooks.com e streamich.imtqy.com .


A seguir, analisaremos as diferenças de sintaxe nos mínimos detalhes e entenderemos o processo de conversão de código de programa, mas antes disso eu gostaria de falar sobre exemplos de uso dessa forma de notação.


Digressão lírica: uso não-padrão da sintaxe de desestruturação


ES2015 deu ao mundo uma coisa maravilhosa como a reestruturação de matrizes . E agora, em vez de extrair cada elemento individualmente:


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

Podemos obter todos os elementos necessários de uma só vez:


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

Esse registro não é apenas mais conciso, mas também menos propenso a erros, pois elimina a necessidade de lembrar índices de elementos e permite que você se concentre no que é realmente importante: inicialização de variáveis.


Assim, chegamos à es2015 que, se não fosse pelo es2015 equipe de es2015 não apresentaria uma maneira tão incomum de trabalhar com o estado.


Em seguida, gostaria de considerar várias bibliotecas que usam uma abordagem semelhante.


Tente pegar


Seis meses antes do anúncio de ganchos na reação, tive a ideia de que a desestruturação pode ser usada não apenas para obter dados homogêneos da matriz, mas também para obter informações sobre um erro ou o resultado de uma função, por analogia com retornos de chamada em node.js. Por exemplo, em vez de usar a sintaxe try-catch :


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

O que parece muito complicado, mas carrega pouca informação e nos obriga a usar let , embora não tenhamos planejado alterar os valores das variáveis. Em vez disso, você pode chamar a função try-catch , que fará tudo o que você precisa, economizando nos problemas listados acima:


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

Dessa maneira interessante, nos livramos de todas as construções sintáticas desnecessárias, deixando apenas o necessário. Este método tem as seguintes vantagens:


  • a capacidade de especificar qualquer nome de variável que seja conveniente para nós (ao usar a destruição de objetos, não teríamos esse privilégio, ou melhor, teria seu próprio preço pesado);
  • a capacidade de usar constantes para dados que não são alterados;
  • sintaxe mais concisa, tudo o que poderia ser removido está faltando;

E, novamente, tudo isso graças à sintaxe da desestruturação de matrizes. Sem essa sintaxe, o uso de uma biblioteca seria ridículo:


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

Esse código ainda é válido, mas perde significativamente em comparação com a desestruturação. Também quero adicionar um exemplo da biblioteca try-to-catch , com o advento do async-await a construção try-catch ainda é relevante e pode ser escrita assim:


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

Se a idéia de tal uso de desestruturação me ocorreu, por que não os criadores da reação também, porque, de fato, temos algo como uma função que tem 2 valores de retorno: uma tupla de Haskel.


Sobre essa digressão lírica pode ser concluída e passar para a questão da transformação.


Convertendo uma classe em React Hooks


Para a conversão, usaremos o transformador putt AST, que permite alterar apenas o necessário e o plug-in @ putout / plugin-plugin-react-hooks .


Para converter a classe herdada do Component em uma função usando react-hooks , as seguintes etapas devem ser realizadas:


  • remover bind
  • renomeie métodos privados para públicos (remova "_");
  • mude this.state para usar ganchos
  • altere this.setState para usar ganchos
  • remova this de qualquer lugar
  • converter class em função
  • nas importações use useState vez de Component

Ligação


Instale o putout com o @putout/plugin-react-hooks :


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

Em seguida, crie o arquivo .putout.json :


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

Então tente putout em ação.


Cabeçalho do 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 encontrou 12 lugares que podem ser corrigidos, tente:


 putout --fix button.js 

Agora o button.js fica assim:


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

Implementação de software


Vamos considerar com mais detalhes várias das regras descritas acima.


Remova this de qualquer lugar


Como não usamos classes, todas as expressões no formato this.setEnabled devem ser convertidas em setEnabled .


Para fazer isso, passaremos pelos nós de ThisExpression , que, por sua vez, são filhos da relação com MemberExpression e estão localizados no campo de object , assim:


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

Considere a implementação da regra 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, }); }, }); }; 

No código descrito acima, a função de utilitário traverseClass para encontrar a classe, não é tão importante para uma compreensão geral, mas ainda faz sentido trazê-la, para maior precisão:


Cabeçalho do 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; } 

O teste, por sua vez, pode ser assim:


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

Nas importações, use useState vez de Component


Considere a implementação da regra convert-import-component-to-use-state .


Para substituir as expressões:


 import React, {Component} from 'react' 

em


 import React, {useState} from 'react' 

Você deve processar o nó 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" } } 

Precisamos encontrar ImportDeclaration com source.value = react e, em seguida, percorrer a matriz de specifiers em busca de ImportSpecifier com o 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 o teste mais simples:


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

E assim, examinamos em termos gerais a implementação do software de várias regras, o restante é construído de acordo com um esquema semelhante. Você pode se familiarizar com todos os nós da árvore do arquivo button.js analisados ​​no astexplorer . O código fonte dos plugins descritos pode ser encontrado no repositório .


Conclusão


Hoje analisamos um dos métodos para refatoração automatizada de classes de reação para reagir a ganchos. Atualmente, o @putout/plugin-react-hooks suporta apenas mecanismos básicos, mas pode ser significativamente aprimorado se a comunidade estiver interessada e envolvida. Ficarei feliz em discutir nos comentários comentários, idéias, exemplos de uso, bem como as funcionalidades ausentes.

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


All Articles