Wir pumpen React Hooks mit FRP

Viele React-Entwickler haben die Hooks gemeistert und Euphorie erlebt. Schließlich haben sie ein einfaches und praktisches Toolkit erhalten, mit dem Sie Aufgaben mit deutlich weniger Code implementieren können. Aber bedeutet dies, dass die standardmäßig angebotenen Hooks useState und useReducer alles sind, was wir zur Verwaltung des Status benötigen?


Meiner Meinung nach ist ihre Verwendung in ihrer Rohform nicht sehr bequem, sie können eher als Grundlage für den Aufbau wirklich praktischer Haken für das Staatsmanagement angesehen werden. Reagieren Entwickler selbst stark auf die Entwicklung von benutzerdefinierten Hooks. Warum also nicht? Unter dem Schnitt werden wir uns ein sehr einfaches und verständliches Beispiel ansehen, was mit gewöhnlichen Haken nicht stimmt und wie sie verbessert werden können, so dass sie sich völlig weigern, sie in ihrer reinen Form zu verwenden.


Es gibt ein bestimmtes Feld für die Eingabe, bedingt einen Namen. Und es gibt eine Schaltfläche durch Klicken, auf die wir eine Anfrage an den Server mit dem eingegebenen Namen stellen sollen (eine bestimmte Suche). Es scheint, dass das einfacher sein könnte? Die Lösung ist jedoch alles andere als offensichtlich. Die erste naive Implementierung:


const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; } 

Was ist hier falsch? Wenn der Benutzer, der etwas in das Feld eingibt, das Formular zweimal sendet, funktioniert nur die erste Anfrage für uns, weil Beim zweiten Klick ändert sich die Anforderung nicht und useEffect funktioniert nicht. Wenn Sie sich vorstellen, dass es sich bei unserer Anwendung um einen Ticketsuchdienst handelt und der Benutzer das Formular möglicherweise in bestimmten Abständen immer wieder sendet, ohne Änderungen vorzunehmen, funktioniert eine solche Implementierung bei uns nicht! Die Verwendung des Namens als Abhängigkeit für useEffect ist ebenfalls nicht akzeptabel. Andernfalls wird das Formular sofort gesendet, wenn sich der Text ändert. Nun, man muss Einfallsreichtum zeigen.


 const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(!request)}/> { result && <div>Result: { result }</div> } </div>; } 

Jetzt ändern wir mit jedem Klick die Bedeutung der Anfrage in das Gegenteil, wodurch das gewünschte Verhalten erreicht wird. Dies ist eine sehr kleine und unschuldige Krücke, aber es macht den Code etwas verwirrend zu verstehen. Vielleicht scheint es Ihnen jetzt, dass ich das Problem aus meinem Finger sauge und seine Skala aufblase. Um zu beantworten, ob es wahr ist oder nicht, müssen Sie diesen Code mit anderen Implementierungen vergleichen, die einen aussagekräftigeren Ansatz bieten.


Schauen wir uns dieses Beispiel auf theoretischer Ebene unter Verwendung der Thread-Abstraktion an. Es ist sehr praktisch, um den Status von Benutzeroberflächen zu beschreiben. Wir haben also zwei Streams: Daten, die in das Textfeld eingegeben wurden (Name $), und einen Stream von Klicks auf die Schaltfläche zum Senden des Formulars (klicken Sie auf $). Aus ihnen müssen wir einen dritten, kombinierten Strom von Anfragen an den Server erstellen.


 name$ __(C)____(Ca)_____(Car)____________________(Carl)___________ click$ ___________________________()______()________________()_____ request$ ___________________________(Car)___(Car)_____________(Carl)_ 

Hier ist das Verhalten, das wir erreichen müssen. Jeder Stream hat zwei Aspekte: den Wert, den er hat, und den Zeitpunkt, zu dem Werte durch ihn fließen. In verschiedenen Situationen benötigen wir möglicherweise den einen oder anderen Aspekt oder beides. Sie können dies mit dem Rhythmus und der Harmonie in der Musik vergleichen. Streams, für die nur die Antwortzeit wesentlich ist, werden auch als Signale bezeichnet.


In unserem Fall ist click $ ein reines Signal: Es spielt keine Rolle, welcher Wert durch das Signal fließt (undefiniert / true / Event / was auch immer), es ist nur dann wichtig, wenn dies geschieht. Fallname $
das Gegenteil: Seine Änderungen haben in keiner Weise Änderungen im System zur Folge, aber wir brauchen möglicherweise irgendwann seine Bedeutung. Und aus diesen beiden Strömen müssen wir den dritten machen, indem wir vom ersten zum zweiten Wert nehmen.


Im Fall von Rxjs haben wir dafür einen fast vorgefertigten Operator:


 const names$ = fromEvent(...); const click$ = fromEvent(...); const request$ = click$.pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(...)))); 

Die praktische Verwendung von Rx in React kann jedoch recht unpraktisch sein. Eine geeignetere Option ist die mrr- Bibliothek, die auf den gleichen funktional-reaktiven Prinzipien wie Rx basiert, jedoch speziell für die Verwendung mit React nach dem Prinzip der "Gesamtreaktivität" angepasst und als Haken verbunden ist.


 import useMrr from 'mrr/hooks'; const App = props => { const [state, set] = useMrr(props, { result: [name => fetch('//example.api/' + name).then(data => data.result), '-name', 'submit'], }); return <div> <input value={state.name} onChange={set('name')}/> <input type="submit" value="Check" onClick={set('submit')}/> { state.result && <div>Result: { state.result }</div> } </div>; } 

Die useMrr-Schnittstelle ähnelt useState oder useReducer: Sie gibt ein Statusobjekt (Werte aller Threads) und einen Setter zurück, um Werte in Threads einzufügen. Aber im Inneren ist alles etwas anders: Jedes Statusfeld (= Stream), mit Ausnahme derjenigen, in die wir Werte direkt aus den DOM-Ereignissen eingeben, wird durch eine Funktion und eine Liste übergeordneter Threads beschrieben, deren Änderung dazu führt, dass das untergeordnete Element neu berechnet wird. In diesem Fall werden die Werte der übergeordneten Threads in die Funktion eingesetzt. Wenn wir nur den Wert des Streams abrufen möchten, aber nicht auf seine Änderung reagieren möchten, schreiben wir wie im Fall des Namens ein "Minus" vor den Namen.


Wir haben das gewünschte Verhalten im Wesentlichen in einer Zeile. Dies ist jedoch nicht nur Kürze. Vergleichen wir die erhaltenen Ergebnisse detaillierter und zunächst in Bezug auf einen Parameter wie Lesbarkeit und Klarheit des resultierenden Codes.


In mrr können Sie die "Logik" vollständig von der "Vorlage" trennen: Sie müssen keine komplexen imperativen Handler in JSX schreiben. Alles ist äußerst deklarativ: Wir ordnen das DOM-Ereignis einfach praktisch ohne Konvertierung dem entsprechenden Stream zu (für Eingabefelder wird der Wert e.target.value automatisch extrahiert, sofern Sie nichts anderes angeben), und bereits in der useMrr-Struktur beschreiben wir, wie die Basisflüsse gebildet werden Tochterunternehmen. Somit können wir sowohl bei synchronen als auch bei asynchronen Datentransformationen immer leicht verfolgen, wie unser Wert gebildet wird.


Vergleich mit Px: Wir mussten nicht einmal zusätzliche Operatoren verwenden: Wenn die mrr-Funktionen als Ergebnis ein Versprechen erhalten, warten sie automatisch, bis sie aufgelöst werden, und fügen die empfangenen Daten in den Stream ein. Außerdem haben wir anstelle von withLatestFrom verwendet
passives Hören (Minuszeichen), was bequemer ist. Stellen Sie sich vor, wir müssen neben dem Namen auch andere Felder senden. Dann werden wir in mrr einen weiteren passiv hörenden Stream hinzufügen:


 result: [(name, surname) => fetch(...), '-name', '-surname', 'submit'], 

Und in Rx müssen Sie mit LatestFrom eine weitere mit einer Karte formen oder zuerst Vor- und Nachnamen in einem Stream kombinieren.


Aber zurück zu Hooks und Mr. Eine besser lesbare Aufzeichnung von Abhängigkeiten, die immer zeigt, wie Daten gebildet werden, ist möglicherweise einer der Hauptvorteile. Die aktuelle useEffect-Schnittstelle erlaubt es grundsätzlich nicht, auf Signalströme zu reagieren, weshalb
Ich muss mir verschiedene Wendungen einfallen lassen.


Ein weiterer Punkt ist, dass die Option gewöhnlicher Hooks zusätzliche Renderings enthält. Wenn der Benutzer nur auf die Schaltfläche geklickt hat, sind noch keine Änderungen an der Benutzeroberfläche erforderlich, die die Reaktion zeichnen muss. Es wird jedoch ein Render aufgerufen. In der Variante mit mrr wird der zurückgegebene Status nur aktualisiert, wenn bereits eine Antwort vom Server eingetroffen ist. Sie sparen Streichhölzer, sagen Sie? Na ja, vielleicht. Aber für mich persönlich führt das Prinzip, "sich in einer unverständlichen Situation neu zu rendern", das die Grundlage für grundlegende Haken darstellt, zur Ablehnung.


Zusätzliche Renderings bedeuten eine neue Formation von Event-Handlern. Übrigens sind hier gewöhnliche Haken alle schlecht. Handler sind nicht nur unerlässlich, sie müssen auch bei jedem Rendern neu generiert werden. Und es wird hier nicht möglich sein, das Caching vollständig zu nutzen, weil Viele Handler müssen an interne Komponentenvariablen gebunden sein. Die mrr-Handler sind deklarativer, und das Caching ist bereits in mrr integriert: set ('name') wird nur einmal generiert und für nachfolgende Renderings aus dem Cache ersetzt.


Mit einer Erhöhung der Codebasis können imperative Handler noch umständlicher werden. Angenommen, wir müssen auch die Anzahl der vom Benutzer vorgenommenen Formularübermittlungen anzeigen.


 const App = () => { const [request, makeRequest] = useState(); const [name, setName] = useState(''); const [result, setResult] = useState(false); const [clicks, setClicks] = useState(0); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => { makeRequest(!request); setClicks(clicks + 1); }}/><br /> Clicked: { clicks } </div>; } 

Nicht sehr gut aussehend. Sie können den Handler natürlich als separate Funktion innerhalb der Komponente rendern. Die Lesbarkeit wird erhöht, aber das Problem der Neuerstellung der Funktion mit jedem Rendering sowie das Problem der Imperativität bleiben bestehen. Im Wesentlichen handelt es sich hierbei um einen regulären Verfahrenscode, obwohl allgemein angenommen wird, dass sich die React-API allmählich in Richtung eines funktionalen Ansatzes ändert.


Für diejenigen, denen das Ausmaß des Problems übertrieben erscheint, kann ich antworten, dass beispielsweise die Entwickler des React selbst sich des Problems der übermäßigen Generierung von Handlern bewusst sind und uns sofort eine Krücke in Form von useCallback anbieten.


Auf mrr:


 const App = props => { const [state, set] = useMrr(props, { $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); return <div> <input onChange={set('name')}/> <input type="submit" value="Check" onClick={set('makeRequest')}/> </div>; } 

Eine bequemere Alternative ist useReducer, mit der Sie den Imperativ der Handler aufgeben können. Andere wichtige Probleme bleiben jedoch bestehen: mangelnde Arbeit mit Signalen (da derselbe useEffect für Nebenwirkungen verantwortlich ist) sowie die schlechteste Lesbarkeit bei asynchronen Konvertierungen (mit anderen Worten, es ist aufgrund des gleichen useEffect schwieriger, die Beziehung zwischen den Feldern des Geschäfts zu verfolgen ) Wenn in mrr das Abhängigkeitsdiagramm zwischen Statusfeldern (Threads) sofort deutlich sichtbar ist, müssen Sie in Hooks Ihre Augen ein wenig auf und ab bewegen.


Außerdem ist es nicht sehr praktisch, useState und useReducer in derselben Komponente freizugeben (auch hier gibt es komplexe Imperative-Handler, die etwas in useState ändern
und Versandaktion), weshalb Sie höchstwahrscheinlich vor der Entwicklung der Komponente die eine oder andere Option akzeptieren müssen.


Natürlich kann die Berücksichtigung aller Aspekte weiterhin fortgesetzt werden. Um den Rahmen des Artikels nicht zu überschreiten, werde ich einige weniger wichtige Punkte vollständig ansprechen.


Zentralisierte Protokollierung, Debugging. Da in mrr alle Streams in einem Hub enthalten sind, reicht es zum Debuggen aus, ein Flag hinzuzufügen:


 const App = props => { const [state, set] = useMrr(props, { $log: true, $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); ... 

Danach werden alle Änderungen an den Streams in der Konsole angezeigt. Um auf den gesamten Status (d. H. Die aktuellen Werte aller Threads) zuzugreifen, gibt es einen Pseudo-Stream-Status $:


 a: [({ name, click, result }) => { ... }, '$state', 'click'], 

Wenn Sie den redaktionellen Stil benötigen oder sehr daran gewöhnt sind, können Sie in mrr im Editorstil schreiben und einen neuen Feldwert zurückgeben, der auf dem Ereignis und dem gesamten vorherigen Status basiert. Das Gegenteil (Schreiben auf useReducer oder einem Editor im mrr-Stil) funktioniert jedoch nicht, da diese nicht reaktiv sind.


Arbeite mit der Zeit. Erinnern Sie sich an zwei Aspekte von Flüssen: Bedeutung und Reaktionszeit, Harmonie und Rhythmus? Das Arbeiten mit dem ersten in gewöhnlichen Haken ist also recht einfach und bequem, aber mit dem zweiten - nein. Mit Arbeiten im Laufe der Zeit meine ich die Bildung von Kinderströmen, deren "Rhythmus" sich vom des Elternteils unterscheidet. Dies sind in erster Linie alle Arten von Filtern, Debowns, Trab usw. All dies müssen Sie höchstwahrscheinlich selbst implementieren. In mrr können Sie sofort vorgefertigte Anweisungen verwenden. Das Gentleman-Set mrr ist der Vielzahl der Operatoren Rx unterlegen, hat jedoch eine intuitivere Benennung.


Intercomponent Interaktion. Ich erinnere mich, dass es im Editor als gute Praxis angesehen wurde, nur eine Geschichte zu erstellen. Wenn wir useReducer in vielen Komponenten verwenden,
Möglicherweise liegt ein Problem bei der Organisation der Interaktion zwischen den Parteien vor. Auf mrr können Flüsse frei von einer Komponente zur anderen "nach oben oder unten" in der Hierarchie "fließen", was jedoch aufgrund des deklarativen Ansatzes keine Probleme verursacht. Ausführlicher
Dieses Thema sowie weitere Funktionen der mrr-API werden im Artikel Actors + FRP in React beschrieben


Schlussfolgerungen


Die neuen Reaktionshaken sind großartig und vereinfachen unser Leben, aber sie haben einige Mängel, die ein übergeordneter Universalhaken (State Management) beheben kann. UseMrr aus der funktional-reaktiven mrr-Bibliothek wurde vorgeschlagen und als solche betrachtet.


Probleme und ihre Lösungen:


  • unnötige Nachzählungen von Daten bei jedem Rendering (in mrr fehlen aufgrund der Push-basierten Reaktivität)
  • Zusätzliche Renderings, wenn eine Änderung des Status keine Änderung der Benutzeroberfläche zur Folge hat
  • schlechte Code-Lesbarkeit bei asynchronen Konvertierungen (im Vergleich zu synchronen). In mrr ist asynchroner Code in seiner Lesbarkeit und Ausdruckskraft dem synchronen nicht unterlegen. Die meisten Probleme, die in einem kürzlich erschienenen Artikel über useEffect auf mrr behandelt wurden, sind grundsätzlich unmöglich
  • Imperative Handler, die nicht immer zwischengespeichert werden können (in mrr werden sie automatisch zwischengespeichert, können fast immer zwischengespeichert werden, deklarativ)
  • Wenn Sie useState und useReducer gleichzeitig verwenden, kann dies zu unangenehmem Code führen
  • Mangel an Werkzeugen zur Umwandlung von Flüssen im Laufe der Zeit (Entprellen, Drosseln, Rennbedingung)

In vielen Punkten kann man argumentieren, dass sie durch benutzerdefinierte Haken gelöst werden können. Genau dies wird vorgeschlagen, aber anstelle unterschiedlicher Implementierungen wird für jede einzelne Aufgabe eine ganzheitliche, konsistente Lösung vorgeschlagen.


Viele Probleme sind zu vertraut geworden, als dass wir sie klar erkennen könnten. Beispielsweise sahen asynchrone Konvertierungen immer komplizierter und verwirrender aus als synchrone, und Hooks in diesem Sinne sind nicht schlechter als frühere Ansätze (Eds usw.). Um dies als Problem zu erkennen, müssen Sie zuerst andere Ansätze sehen, die eine bessere Lösung bieten.


Dieser Artikel soll keine spezifischen Ansichten auferlegen, sondern auf das Problem aufmerksam machen. Ich bin sicher, dass es andere Lösungen gibt oder gibt, die eine würdige Alternative darstellen können, aber noch nicht allgemein bekannt sind. Die bevorstehende React Cache API kann ebenfalls einen großen Unterschied machen. Ich werde mich über Kritik und Diskussion in den Kommentaren freuen.


Interessenten können sich am 28. März auch eine Präsentation zu diesem Thema auf kyivjs ansehen.

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


All Articles