React 16.18 ist die erste stabile Version mit Unterstützung für React Hooks . Jetzt können Sie Hooks verwenden, ohne befürchten zu müssen, dass sich die API dramatisch ändert. Und obwohl das react
Entwicklungsteam empfiehlt, die neue Technologie nur für neue Komponenten zu verwenden, möchten viele, einschließlich mir, sie für ältere Komponenten verwenden, die Klassen verwenden. Da manuelles Refactoring jedoch ein mühsamer Prozess ist, werden wir versuchen, ihn zu automatisieren. Die in diesem Artikel beschriebenen Techniken eignen sich zur Automatisierung des Refactorings nicht nur von react
, sondern auch von jedem anderen JavaScript
Code.
Eigenschaften React Hooks
Der Einführungsartikel zu React Hooks beschreibt, was die Hooks sind und womit sie essen. Kurz gesagt, dies ist eine neue verrückte Technologie zum Erstellen state
Komponenten ohne Verwendung von Klassen.
Betrachten Sie die Datei 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} /> ); } }
Mit Haken sieht es so aus:
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} /> ); }
Sie können lange darüber streiten, wie diese Art der Aufzeichnung für Personen, die mit der Technologie nicht vertraut sind, offensichtlicher ist, aber eines ist sofort klar: Der Code ist prägnanter und einfacher wiederzuverwenden. Interessante Sätze von benutzerdefinierten Hooks finden Sie unter usehooks.com und streamich.imtqy.com .
Als nächstes werden wir die Syntaxunterschiede bis ins kleinste Detail analysieren und den Prozess der Programmcodekonvertierung verstehen, aber vorher möchte ich über Beispiele für die Verwendung dieser Form der Notation sprechen.
Lyrischer Exkurs: Nicht standardmäßige Verwendung der Destrukturierungssyntax
ES2015
gab der Welt so etwas Wunderbares wie die Umstrukturierung von Arrays . Und jetzt, anstatt jedes Element einzeln zu extrahieren:
const letters = ['a', 'b']; const first = letters[0]; const second = letters[1];
Wir können alle notwendigen Elemente auf einmal erhalten:
const letters = ['a', 'b']; const [first, second] = letters;
Ein solcher Datensatz ist nicht nur präziser, sondern auch weniger fehleranfällig, da keine Elementindizes mehr gespeichert werden müssen und Sie sich auf das konzentrieren können, was wirklich wichtig ist: die Initialisierung von Variablen.
Wir kommen daher zu dem es2015
dass es2015
ohne es2015
keine so ungewöhnliche Art der Zusammenarbeit mit dem Staat entwickeln würde.
Als nächstes möchte ich einige Bibliotheken betrachten, die einen ähnlichen Ansatz verwenden.
Versuchen Sie es mit fangen
Sechs Monate vor der Ankündigung von Hooks in der Reaktion hatte ich die Idee, dass Destrukturierung nicht nur verwendet werden kann, um homogene Daten aus dem Array zu erhalten, sondern auch, um analog Informationen über einen Fehler oder das Ergebnis einer Funktion zu erhalten mit Rückrufen in node.js. Zum Beispiel, anstatt die try-catch
Syntax zu verwenden:
let data; let error; try { data = JSON.parse('xxxx'); } catch (e) { error = e; }
Das sieht sehr umständlich aus, enthält aber nur wenige Informationen und zwingt uns, let
zu verwenden, obwohl wir nicht vorhatten, die Werte der Variablen zu ändern. Stattdessen können Sie die Try-Catch- Funktion aufrufen, die alles tut, was Sie benötigen, und uns vor den oben aufgeführten Problemen bewahrt:
const [error, data] = tryCatch(JSON.parse, 'xxxx');
Auf diese interessante Weise haben wir alle unnötigen syntaktischen Konstruktionen beseitigt und nur das Notwendige übrig gelassen. Diese Methode hat folgende Vorteile:
- die Möglichkeit, Variablennamen anzugeben, die für uns günstig sind (wenn wir die Destrukturierung von Objekten verwenden, hätten wir kein solches Privileg, oder vielmehr hätte es einen eigenen umständlichen Preis);
- die Fähigkeit, Konstanten für Daten zu verwenden, die sich nicht ändern;
- Bei einer präziseren Syntax fehlt alles, was entfernt werden könnte.
Und das alles dank der Syntax der Destrukturierung von Arrays. Ohne diese Syntax würde die Verwendung einer Bibliothek lächerlich aussehen:
const result = tryCatch(JSON.parse, 'xxxx'); const error = result[0]; const data = result[1];
Dies ist immer noch gültiger Code, verliert jedoch im Vergleich zur Destrukturierung erheblich. Ich möchte auch ein Beispiel für die Arbeit der Try-to-Catch- Bibliothek hinzufügen. Mit dem Aufkommen von async-await
das try-catch
Konstrukt immer noch relevant und kann folgendermaßen geschrieben werden:
const [error, data] = await tryToCatch(readFile, path, 'utf8');
Wenn mir die Idee eines solchen Einsatzes von Destrukturierung gekommen ist, warum dann nicht auch die Schöpfer der Reaktion, denn tatsächlich haben wir so etwas wie eine Funktion, die zwei Rückgabewerte hat: ein Haskel-Tupel.
Auf diesem lyrischen Exkurs kann abgeschlossen werden und zum Thema Transformation übergehen.
Konvertieren einer Klasse in React Hooks
Für die Konvertierung verwenden wir den Putout- AST-Transformator, mit dem Sie nur das Notwendige ändern können, und das @ putout / plugin-react-hooks-Plugin .
Um die von Component
geerbte Klasse mithilfe von react-hooks
in eine Funktion zu konvertieren, müssen die folgenden Schritte ausgeführt werden:
bind
entfernen- private Methoden in public umbenennen ("_" entfernen);
- Ändern Sie
this.state
, um Hooks zu verwenden - Ändern Sie
this.setState
, um Hooks zu verwenden - entfernen Sie
this
von überall class
in Funktion konvertieren- Verwenden
useState
beim Importieren useState
anstelle von Component
Verbindung
Installieren Sie putout
mit dem @putout/plugin-react-hooks
:
npm i putout @putout/plugin-react-hooks -D
Erstellen Sie als .putout.json
Datei .putout.json
:
{ "plugins": [ "react-hooks" ] }
Dann versuchen Sie es mit putout
in Aktion.
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
hat 12 Stellen gefunden, die putout
können. Versuchen Sie:
putout --fix button.js
Jetzt sieht button.js
aus:
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} /> ); }
Software-Implementierung
Lassen Sie uns einige der oben beschriebenen Regeln genauer betrachten.
Entfernen Sie this
von überall
Da wir keine Klassen verwenden, müssen alle Ausdrücke der Form this.setEnabled
in setEnabled
konvertiert setEnabled
.
Dazu gehen wir die Knoten von ThisExpression durch , die wiederum untergeordnete Elemente der Beziehung zu MemberExpression sind und sich im object
{ "type": "MemberExpression", "object": { "type": "ThisExpression", }, "property": { "type": "Identifier", "name": "setEnabled" } }
Betrachten Sie die Implementierung der Remove-This- Regel:
In dem oben beschriebenen Code wird die Dienstprogrammfunktion traverseClass
, um die Klasse zu finden. Dies ist für ein allgemeines Verständnis nicht so wichtig, aber es ist dennoch sinnvoll, sie für eine größere Genauigkeit zu verwenden:
Der Test kann wiederum so aussehen:
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(); });
Verwenden useState
Importieren useState
anstelle von Component
Betrachten Sie die Implementierung der Regel zum Konvertieren der Importkomponente in den Verwendungsstatus .
Um die Ausdrücke zu ersetzen:
import React, {Component} from 'react'
auf
import React, {useState} from 'react'
Sie müssen den ImportDeclaration- Knoten verarbeiten:
{ "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" } }
Wir müssen ImportDeclaration
mit source.value = react
finden und dann das ImportSpecifier
Array auf der Suche nach ImportSpecifier
mit dem Feld name = Component
durchsuchen:
Betrachten Sie den einfachsten 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(); });
Und so haben wir allgemein die Software-Implementierung mehrerer Regeln untersucht, der Rest ist nach einem ähnlichen Schema aufgebaut. Sie können alle Knoten des Baums der analysierten Datei button.js in astexplorer kennenlernen . Der Quellcode der beschriebenen Plugins befindet sich im Repository .
Fazit
Heute haben wir uns eine der Methoden zum automatisierten Refactoring von Reaktionsklassen für Reaktionshaken angesehen. Derzeit unterstützt das @putout/plugin-react-hooks
nur grundlegende Mechanismen, kann jedoch erheblich verbessert werden, wenn die Community interessiert und involviert ist. Ich werde gerne in den Kommentaren Kommentare, Ideen, Anwendungsbeispiele sowie die fehlende Funktionalität diskutieren.