React 16.18 is the first stable release with support for react hooks . Now you can use hooks without fear that the API will change drastically. And although the react
development react
advises to use new technology only for new components, many people, including me, would like to use them for old components using classes. But since manual refactoring is a laborious process, we will try to automate it. The techniques described in this article are suitable for automating the refactoring of not only react
components, but also any other JavaScript
code.
An introduction to React Hooks explains in detail what the hooks are and what they eat. In a nutshell, this is a new crazy technology for creating components with state
without using classes.
Consider the button.js
file:
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} /> ); } }
With hooks, it will look like this:
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} /> ); }
One can argue for a long time how such a type of record is more obvious to people unfamiliar with technology, but one thing is clear right away: the code is more concise and easier to reuse. Interesting sets of custom hooks can be found at usehooks.com and streamich.imtqy.com .
Next, we will examine the syntactical differences in the smallest details, and deal with the process of software code conversion, but before that I would like to talk about examples of using this form of writing.
ES2015
presented the world with such a wonderful thing as array ES2015
. And now instead of extracting each element separately:
const letters = ['a', 'b']; const first = letters[0]; const second = letters[1];
We can immediately get all the necessary elements:
const letters = ['a', 'b']; const [first, second] = letters;
Such a record is not only more concise, but also less error prone, since it removes the need to remember about the indexes of elements, and allows you to focus on what is really important: the initialization of variables.
Thus, we come to the es2015
that if not for es2015
team had not come up with such an unusual way of working with the state.
Next, I would like to look at several libraries that use a similar approach.
Six months before the announcement of hooks in the reactor, I got the idea that destructuring can be used not only to get homogeneous data from the array, but also to get information about the error or the result of the function, by analogy with callbacks in node.js. For example, instead of using the try-catch
syntax:
let data; let error; try { data = JSON.parse('xxxx'); } catch (e) { error = e; }
What looks very cumbersome, while carrying enough little information, and forcing us to use let
, although we did not plan to change the values of variables. Instead, you can call the function try-catch , which will do everything you need, saving us from the problems listed above:
const [error, data] = tryCatch(JSON.parse, 'xxxx');
In such an interesting way, we got rid of all unnecessary syntactic structures, leaving only the necessary. This method has the following advantages:
And, again, all this is due to the syntax of array restructuring. Without this syntax, using the library would look ridiculous:
const result = tryCatch(JSON.parse, 'xxxx'); const error = result[0]; const data = result[1];
This is still a valid code, but it loses significantly compared to destructuring. I also want to add an example of the work of the try-to-catch library, with the arrival of async-await
the try-catch
construction is still relevant, and can be written like this:
const [error, data] = await tryToCatch(readFile, path, 'utf8');
If the idea of such a use of de-structuring came to me, then why shouldn't the creators of the reactor come to it, after all, in fact, we have something like a function that has 2 return values: a tuple from a Haskel.
At this lyrical digression, you can finish and go to the question of conversion.
For the conversion, we will use the AST transformer putout , which allows changing only what is needed and the @ putout / plugin-react-hooks plugin .
In order to convert a class inherited from Component
into a function that uses react-hooks
, the following steps must be done:
bind
this.state
to use hooksthis.setState
to use hooksthis
from everywhereclass
to functionuseState
instead of Component
Install putout
along with the @putout/plugin-react-hooks
:
npm i putout @putout/plugin-react-hooks -D
Next, create the .putout.json
file:
{ "plugins": [ "react-hooks" ] }
Then try putout
in action.
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
found 12 places that can be fixed, try:
putout --fix button.js
Now button.js
looks like this:
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} /> ); }
Let us consider in more detail several of the rules described above.
this
from everywhereSince we do not use classes, all expressions of the form this.setEnabled
must be converted to setEnabled
.
To do this, we will go through the ThisExpression nodes, which, in turn, are children of the relation to MemberExpression , and are located in the object
field, thus:
{ "type": "MemberExpression", "object": { "type": "ThisExpression", }, "property": { "type": "Identifier", "name": "setEnabled" } }
Consider the implementation of the remove-this rule:
// 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, }); }, }); };
In the code described above, the traverseClass
utility function is used to find the class, it is not so important for a common understanding, but it still makes sense to bring it in, for greater accuracy:
// 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; }
The test, in turn, may look like this:
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
instead of Component
Consider the implementation of the convert-import-component-to-use-state rule.
To replace expressions:
import React, {Component} from 'react'
on
import React, {useState} from 'react'
It is necessary to process the ImportDeclaration node:
{ "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" } }
We need to find ImportDeclaration
with source.value = react
, and then bypass the specifiers
array in the search for an ImportSpecifier
with the name = Component
field:
// 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); } }, }); };
Consider the simplest test:
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(); });
And so, we have reviewed in general terms the software implementation of several rules, the rest are based on a similar scheme. You can get acquainted with all the nodes of the tree of the button.js
file being parsed in astexplorer . The source code for the plugins described can be found in the repository .
Today we looked at one of the methods of automated refactoring of classes of reactors to reactants. Currently, the @putout/plugin-react-hooks
only supports basic mechanisms, but it can be significantly improved if the community is interested and involved. I would be happy to discuss comments, ideas, examples of use, as well as the missing functionality in the comments.
Source: https://habr.com/ru/post/441722/
All Articles