自动过渡到React Hooks

React 16.18是第一个支持react钩子的稳定版本。 现在,您可以使用挂钩,而不必担心API会发生巨大变化。 尽管react开发react建议仅将新技术用于新组件,但包括我在内的许多人仍希望将其用于使用类的旧组件。 但是由于手动重构是一个费力的过程,因此我们将尝试使其自动化。 本文描述的技术不仅适用于自动重构react组件,还适用于任何其他JavaScript代码的自动化。


特点React挂钩


React Hooks简介”文章详细介绍了什么是钩子以及它们与之一起吃。 简而言之,这是一种无需使用类即可创建无state组件的疯狂新技术。


考虑一下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} /> ); } } 

带有钩子,它将如下所示:


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

您可能会争论很长时间,这种记录对于不熟悉该技术的人来说如何更加明显,但是有一点很明显:代码更简洁,更易于重用。 有趣的自定义钩子集可以在usehooks.comstreamich.imtqy.com上找到。


接下来,我们将分析语法差异的最小细节,并处理程序代码转换过程,但是在此之前,我想谈谈使用这种形式的符号的示例。


抒情离题:非标准使用解构语法


ES2015给世界带来了很多很棒的事情,例如重组数组 。 现在,而不是分别提取每个元素:


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

我们可以立即获取所有必要的元素:


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

这样的记录不仅更简洁,而且更不容易出错,因为它消除了记住元素索引的需要,并使您可以专注于真正重要的事情:变量的初始化。


因此,我们得出的es2015是,如果不是在es2015团队将不会想出与国家合作的不寻常方式。


接下来,我想考虑几个使用类似方法的库。


赶上


在宣布发生钩子之前六个月,我想到了一个想法,即解构不仅可以用于从数组中获取同类数据,还可以通过类比获取有关错误或函数结果的信息。 在node.js中使用回调。 例如,不要使用try-catch语法:


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

它看起来很麻烦,但是却携带很少的信息,并迫使我们使用let ,尽管我们并不打算更改变量的值。 相反,您可以调用try-catch函数,该函数将完成您需要的所有操作,从而使我们免受上述问题的困扰:


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

通过这种有趣的方式,我们摆脱了所有不必要的句法构造,仅保留了必要的句法。 该方法具有以下优点:


  • 能够指定对我们方便的任何变量名的能力(使用对象的销毁时,我们将没有这种特权,或者,它会有其笨拙的价格);
  • 对不变的数据使用常量的能力;
  • 更简洁的语法,所有可以删除的内容都丢失了;

而且,这一切都要归功于数组解构的语法。 如果没有这种语法,使用库将显得很荒谬:


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

这仍然是有效的代码,但是与销毁相比,它损失很多。 我还想添加一个try-to-catch库的示例,随着async-await的问世async-await try-catch构造仍然很重要,可以这样写:


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

如果我想到了使用解构的想法,那么为什么也不要创建反应,因为实际上,我们有一个类似的函数,它具有两个返回值: 一个 Haskel元组。


这样,抒情离题就可以完成,并继续进行转变。


在React Hooks中转换类


为了进行转换,我们将使用放置式 AST转换器,该转换器允许您仅更改所需的内容和@ putout / plugin-react-hooks plugin


为了使用react-hooks将从Component继承的类转换为函数,必须完成以下步骤:


  • 删除bind
  • 将私有方法重命名为public(删除“ _”);
  • 更改this.state以使用钩子
  • 更改this.setState以使用钩子
  • 从任何地方删除
  • class转换为函数
  • 在导入中使用useState而不是Component

连接方式


使用@putout/plugin-react-hooks安装putout


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

接下来,创建.putout.json文件:


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

然后尝试使用putout


扰流板头
 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发现12个可以修复的地方,请尝试:


 putout --fix button.js 

现在button.js看起来像这样:


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

软件实施


让我们更详细地考虑上述几个规则。


从任何地方删除


由于我们不使用类,因此所有形式为this.setEnabled表达式this.setEnabled必须转换为setEnabled


为此,我们将遍历ThisExpression的节点,这些节点又是与MemberExpression关系的子 ,并且位于object字段中,因此:


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

考虑执行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, }); }, }); }; 

在上面描述的代码中,使用实用程序函数traverseClass来查找类,对于一般理解而言,它并不是那么重要,但是为了获得更高的准确性,带上它仍然有意义:


扰流板头
 //      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; } 

该测试反过来可能看起来像这样:


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

在导入中,使用useState而不是Component


考虑实现convert-import-component-to-use状态规则的实现。


为了替换表达式:


 import React, {Component} from 'react' 


 import React, {useState} from 'react' 

您必须处理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" } } 

我们需要找到带有source.value = react ImportDeclaration ,然后在specifiers数组中查找name = ComponentImportSpecifier


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

考虑最简单的测试:


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

因此,我们从总体上检查了几个规则的软件实现,其余的则根据类似的方案进行构建。 您可以在astexplorer中了解button.js解析文件的树的所有节点。 可以在资源库中找到所描述插件的源代码。


结论


今天,我们研究了一种自动重构反应类别以反应挂钩的方法。 当前, @putout/plugin-react-hooks仅支持基本机制,但是如果社区感兴趣并参与其中,则可以大大改善它。 我将很高兴在评论中讨论评论,想法,使用示例以及缺少的功能。

Source: https://habr.com/ru/post/zh-CN441722/


All Articles