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.com和streamich.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规则:
在上面描述的代码中,使用实用程序函数traverseClass
来查找类,对于一般理解而言,它并不是那么重要,但是为了获得更高的准确性,带上它仍然有意义:
该测试反过来可能看起来像这样:
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 = Component
的ImportSpecifier
:
考虑最简单的测试:
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
仅支持基本机制,但是如果社区感兴趣并参与其中,则可以大大改善它。 我将很高兴在评论中讨论评论,想法,使用示例以及缺少的功能。