Mrr: Gesamt-FRP für React

Mrr ist eine funktional-reaktive Bibliothek für React (ich entschuldige mich für die imaginäre Tautologie).

Das Wort "Reaktivität" bezieht sich normalerweise auf Rx.js als Referenz-FRP. Eine Reihe neuerer Artikel zu diesem Thema über Habré ( [1] , [2] , [3] ) zeigte jedoch die Umständlichkeit von Rx-Lösungen, die in einfachen Beispielen gegenüber fast jedem anderen Ansatz an Klarheit und Einfachheit verloren haben. Rx ist groß und leistungsstark und eignet sich perfekt zur Lösung von Problemen, bei denen sich die Abstraktion des Flusses anbietet (in der Praxis ist dies hauptsächlich die Koordination asynchroner Aufgaben). Aber würden Sie zum Beispiel eine einfache synchrone Formularvalidierung auf Rx schreiben? Würde er Ihre Zeit im Vergleich zu den üblichen imperativen Ansätzen sparen?

mrr ist ein Versuch zu beweisen, dass FRF nicht nur bei bestimmten "Streaming" -Problemen, sondern auch bei den häufigsten Routine-Front-End-Aufgaben eine bequeme und effektive Lösung sein kann.

Reaktive Programmierung ist eine sehr mächtige Abstraktion, die im Frontend derzeit auf zwei Arten vorhanden ist:

  • reaktive Variablen (berechnete Variablen): einfach, zuverlässig, intuitiv, aber das Potenzial des RP ist bei weitem nicht vollständig offengelegt
  • Bibliotheken für die Arbeit mit Streams wie Rx, Bacon usw.: Leistungsstark, aber ziemlich kompliziert. Der Umfang der praktischen Verwendung ist auf bestimmte Aufgaben beschränkt.

Mrr kombiniert die Vorteile dieser Ansätze. Im Gegensatz zu Rx.js verfügt mrr über eine kurze API, die der Benutzer mit seinen Ergänzungen erweitern kann. Anstelle von Dutzenden von Methoden und Operatoren - vier grundlegende Operatoren anstelle von Observable (heiß und kalt), Subject usw. - eine Abstraktion: Stream. Außerdem fehlen mrr einige komplexe Konzepte, die die Lesbarkeit des Codes erheblich erschweren können, z. B. Metastreams.

Mrr ist jedoch kein "auf neue Weise vereinfachter Empfang". Basierend auf den gleichen Grundprinzipien wie Rx behauptet mrr, eine größere Nische zu sein: die Verwaltung des globalen und lokalen Anwendungsstatus (auf Komponentenebene). Obwohl das ursprüngliche Konzept der reaktiven Programmierung für asynchrone Aufgaben gedacht war, hat mrr erfolgreich Reaktivitätsansätze für gewöhnliche, synchrone Aufgaben verwendet. Dies ist das Prinzip der „Gesamt-FRP“.

Beim Erstellen einer Anwendung in React werden häufig verschiedene Technologien verwendet: Neuzusammenstellen (oder bald - Hooks) für den Komponentenstatus, Redux / Mobx für den globalen Status, Rx mit redux-beobachtbar (oder Thunk / Saga) zum Verwalten von Nebenwirkungen und Koordinieren von Asynchronität Aufgaben im Editor. Anstelle eines solchen „Salats“ verschiedener Ansätze und Technologien innerhalb derselben Anwendung können Sie mit mrr eine einzige Technologie und ein einziges Paradigma verwenden.

Die mrr-Schnittstelle unterscheidet sich auch erheblich von Rx und ähnlichen Bibliotheken - sie ist deklarativer. Dank der Reaktivitätsabstraktion und des deklarativen Ansatzes können Sie mit mrr ausdrucksstarken und präzisen Code schreiben. Zum Beispiel benötigt der Standard TodoMVC auf mrr weniger als 50 Codezeilen (ohne Berücksichtigung der JSX-Vorlage).

Aber genug Werbung. Haben Sie es geschafft, die Vorteile des "leichten" und "schweren" RP in einer Flasche zu kombinieren - sollten Sie beurteilen, aber lesen Sie zuerst die Codebeispiele.

TodoMVC ist bereits ziemlich schmerzhaft, und das Beispiel des Herunterladens von Daten über Github-Benutzer ist zu primitiv, um die Funktionen der Bibliothek darauf spüren zu können. Wir werden mrr als Beispiel für eine bedingte Anwendung für den Kauf von Bahntickets betrachten. In unserer Benutzeroberfläche gibt es Felder zur Auswahl der Start- und Endstationen sowie der Daten. Nach dem Senden der Daten wird eine Liste der verfügbaren Züge und Plätze in diesen zurückgegeben. Nachdem der Benutzer einen bestimmten Zug und Fahrzeugtyp ausgewählt hat, gibt er die Fahrgastdaten ein und legt die Fahrkarten in den Warenkorb. Lass uns gehen.

Wir brauchen ein Formular mit einer Auswahl an Stationen und Daten:



Erstellen Sie Felder für die automatische Vervollständigung zur Eingabe von Stationen.

import { withMrr } from 'mrr'; const stations = [ '', '', '', ' ', ... ] const Tickets = withMrr({ //    $init: { stationFromOptions: [], stationFromInput: '', }, //   - "" stationFromOptions: [str => stations.filter(s => s.indexOf(str)===0), 'stationFromInput'], }, (state, props, $) => { return (<div> <h3>    </h3> <div>  : <input onChange={ $('stationFromInput') } /> </div> <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li>{ s }</li>) } </ul> </div>); }); export default Tickets; 

Mrr-Komponenten werden mit der Funktion withMrr erstellt, die ein reaktives Verknüpfungsdiagramm (Beschreibung der Flüsse) und eine Renderfunktion akzeptiert. Renderfunktionen werden an die Requisiten der Komponente sowie an den Status übergeben, der jetzt vollständig von mrr gesteuert wird. Es enthält die Initiale (Block $ init) und wird anhand der Formelwerte der reaktiven Zellen berechnet.

Jetzt haben wir zwei Zellen (oder zwei Streams, was dasselbe ist): stationFromInput , die Werte, auf die Benutzereingaben mit dem $ helper fallen (standardmäßig event.target.value für Dateneingabeelemente übergeben), und eine daraus abgeleitete Zelle stationFromOptions , die ein Array übereinstimmender Stationen nach Namen enthält.

Der Wert von stationFromOptions wird automatisch jedes Mal berechnet, wenn eine übergeordnete Zelle mithilfe einer Funktion geändert wird (in der mrr-Terminologie „ Formel “ - analog zu Excel-Formeln). Die Syntax von mrr-Ausdrücken ist einfach: Erstens die Funktion (oder der Operator), mit der der Wert der Zelle berechnet wird, dann gibt es eine Liste von Zellen, von denen diese Zelle abhängt: Ihre Werte werden an die Funktion übergeben. Eine solch seltsame Syntax hat auf den ersten Blick viele Vorteile, die wir später betrachten werden. Bisher erinnert die mrr-Logik hier an den üblichen Ansatz mit berechenbaren Variablen, die in Vue, Svelte und anderen Bibliotheken verwendet werden, mit dem einzigen Unterschied, dass Sie reine Funktionen verwenden können.

Wir implementieren die Ersetzung der ausgewählten Station aus der Liste im Eingabefeld. Es ist auch erforderlich, die Liste der Stationen auszublenden, nachdem der Benutzer auf eine dieser Stationen geklickt hat.

 const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'], }, (state, props, $) => { return (<div> <div>  : <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>); }); 

Ein Ereignishandler, der mit dem $ helper in der Liste der Stationen erstellt wurde, gibt für jede Option feste Werte aus.

mrr ist in seinem deklarativen Ansatz konsistent und allen Mutationen fremd. Nach Auswahl einer Station können wir den Zellenwert nicht mehr „erzwingen“. Stattdessen erstellen wir eine neue stationFrom- Zelle, die mithilfe des Merge-Stream- Merge- Operators (ein ungefähres Äquivalent für Rx ist combinLatest) die Werte von zwei Streams sammelt: Benutzereingabe ( stationFromInput ) und Stationsauswahl ( selectStationFrom ).

Wir müssen die Liste der Optionen anzeigen, nachdem der Benutzer etwas eingegeben hat, und ausblenden, nachdem er eine der Optionen ausgewählt hat. Die Zelle optionsShown ist für die Sichtbarkeit der Liste der Optionen verantwortlich, die abhängig von Änderungen in anderen Zellen boolesche Werte annehmen. Dies ist ein sehr verbreitetes Muster, für das syntaktischer Zucker existiert - der Umschaltoperator . Der Wert der Zelle wird bei jeder Änderung des ersten Arguments (Streams) auf true und bei der zweiten auf false gesetzt .

Fügen Sie eine Schaltfläche hinzu, um den eingegebenen Text zu löschen.

 const Tickets = withMrr({ $init: { stationFromOptions: [], stationFromInput: '', }, stationFromOptions: [str => stations.filter(s => str.indexOf(a) === 0), 'stationFromInput'], clearVal: [a => '', 'clear'], stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', 'clearVal'], optionsShown: ['toggle', 'stationFromInput', 'selectStationFrom'], }, (state, props, $) => { return (<div> <div>  : <input onChange={ $('stationFromInput') } value={ state.stationFrom }/> { state.stationFrom && <button onClick={ $('clear') }></button> } </div> { state.optionsShown && <ul className="stationFromOptions"> { state.stationFromOptions.map(s => <li onClick={ $('selectStationFrom', s) }>{ s }</li>) } </ul> } </div>); }); 

Jetzt sammelt unsere stationFrom- Zelle, die für den Inhalt des Textes im Eingabefeld verantwortlich ist, ihre Werte nicht aus zwei, sondern aus drei Streams. Dieser Code kann vereinfacht werden. Das mrr-Konstrukt der Form [* Formel *, * ... Zellargumente *] ähnelt S-Ausdrücken in Lisp, und wie in Lisp können Sie solche Konstruktionen beliebig ineinander verschachteln.

Lassen Sie uns die nutzlose clearVal-Zelle loswerden und den Code verkürzen:

  stationFrom: ['merge', 'stationFromInput', 'selectStationFrom', [a => '', 'clear']], 

Programme, die in einem imperativen Stil geschrieben wurden, können mit einem schlecht organisierten Team verglichen werden, in dem sich alle ständig gegenseitig etwas bestellen (ich weise auf Methodenaufrufe und mutierende Änderungen hin). Außerdem sind beide Manager untergeordnet und umgekehrt. Deklarative Programme ähneln dem entgegengesetzten utopischen Bild: Ein Kollektiv, in dem jeder genau weiß, wie er in jeder Situation handeln soll. In einem solchen Team sind keine Bestellungen erforderlich, jeder ist nur an seinem Platz und arbeitet als Reaktion auf das, was gerade passiert.

Anstatt alle möglichen Konsequenzen eines Ereignisses zu beschreiben (lesen - um bestimmte Mutationen vorzunehmen), beschreiben wir alle Fälle, in denen dieses Ereignis auftreten kann, d. H. Welchen Wert nimmt die Zelle bei Änderungen in anderen Zellen an? In unserem bisher kleinen Beispiel haben wir die stationFrom-Zelle und drei Situationen beschrieben, die sich auf ihren Wert auswirken. Für einen Programmierer, der an den imperativen Code gewöhnt ist, mag ein solcher Ansatz ungewöhnlich erscheinen (oder sogar eine „Krücke“, eine „Perversion“). In der Tat können Sie aufgrund der Kürze (und Stabilität) des Codes Aufwand sparen, wie wir in der Praxis sehen werden.

Was ist mit Asynchronität? Ist es möglich, die Liste der vorgeschlagenen Sender mit Ajax aufzurufen? Kein Problem! Im Wesentlichen spielt es für mrr keine Rolle, ob die Funktion einen Wert oder ein Versprechen zurückgibt. Wenn mrr zurückgegeben wird, wartet es auf seine Auflösung und "pusht" die empfangenen Daten in den Stream.

 stationFromOptions: [str => fetch('/get_stations?str=' + str).then(res => res.toJSON()), 'stationFromInput'], 

Dies bedeutet auch, dass Sie asynchrone Funktionen als Formeln verwenden können. Komplexere Fälle (Fehlerbehandlung, Versprechungsstatus) werden später berücksichtigt.

Die Funktionalität zur Auswahl einer Abfahrtsstation ist bereit. Es macht keinen Sinn, dasselbe für die Ankunftsstation zu duplizieren. Es lohnt sich, es in eine separate Komponente zu packen, die wiederverwendet werden kann. Dies wird eine verallgemeinerte Eingabekomponente mit automatischer Vervollständigung sein, daher werden wir die Felder umbenennen und die Funktion zum Abrufen geeigneter Optionen in Requisiten festlegen.

 const OptionsInput = withMrr(props => ({ $init: { options: [], }, val: ['merge', 'valInput', 'selectOption', [a => '', 'clear']], options: [props.getOptions, 'val'], optionsShown: ['toggle', 'valInput', 'selectOption'], }), (state, props, $) => <div> <div> <input onChange={ $('valInput') } value={ state.val } /> </div> { state.optionsShown && <ul className="options"> { state.options.map(s => <li onClick={ $('selectOption', s) }>{ s }</li>) } </ul> } { state.val && <div className="clear" onClick={ $('clear') }> X </div> } </div>) 

Wie Sie sehen können, können Sie die Struktur von mrr-Zellen als Funktion der Requisiten-Komponente angeben (sie wird jedoch nur einmal ausgeführt - bei der Initialisierung und reagiert nicht auf Änderungen an Requisiten).

Kommunikation zwischen Komponenten


Verbinden Sie nun diese Komponente mit der übergeordneten Komponente und sehen Sie, wie mrr verwandten Komponenten den Datenaustausch ermöglicht.

 const getMatchedStations = str => fetch('/get_stations?str=' + str).then(res => res.toJSON()); const Tickets = withMrr({ stationTo: 'selectStationFrom/val', stationFrom: 'selectStationTo/val', }, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div>); }); 

Um eine übergeordnete Komponente einer untergeordneten Komponente zuzuordnen , müssen wir ihr Parameter mithilfe der Funktion connectAs (viertes Argument der Renderfunktion ) übergeben. In diesem Fall geben wir den Namen an, den wir der untergeordneten Komponente geben möchten. Durch Anhängen einer Komponente auf diese Weise können wir unter diesem Namen auf ihre Zellen zugreifen. In diesem Fall hören wir Val-Zellen. Das Gegenteil ist ebenfalls möglich - hören Sie von der untergeordneten Komponente der übergeordneten Zelle ab.

Wie Sie sehen können, folgt mrr hier einem deklarativen Ansatz: Es sind keine onChange-Rückrufe erforderlich, wir müssen lediglich einen Namen für die untergeordnete Komponente in der connectAs-Funktion angeben, wonach wir Zugriff auf ihre Zellen erhalten! Auch in diesem Fall besteht aufgrund der Deklarativität keine Gefahr einer Störung der Arbeit einer anderen Komponente - wir haben nicht die Möglichkeit, etwas daran zu „ändern“, zu mutieren, wir können nur auf die Daten „hören“.

Signale und Werte


Die nächste Stufe ist die Suche nach geeigneten Zügen für die ausgewählten Parameter. Im imperativen Ansatz würden wir wahrscheinlich einen bestimmten Prozessor schreiben, um das onSubmit-Formular zu senden, das weitere Aktionen einleiten würde - eine Ajax-Anforderung und die Anzeige der Ergebnisse. Aber wie Sie sich erinnern, können wir nichts "bestellen"! Wir können nur einen weiteren Satz von Zellen erstellen, die von den Zellen des Formulars abgeleitet sind. Schreiben wir noch eine Anfrage.

 const getTrains = (from, to, date) => fetch('/get_trains?from=' + from + '&to=' + to + '&date=' + date).then(res => res.toJSON()); const Tickets = withMrr({ stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', results: [getTrains, 'stationFrom', 'stationTo', 'date', 'searchTrains'], }, (state, props, $, connectAs) => { return (<div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div>); }); 

Dieser Code funktioniert jedoch nicht wie erwartet. Die Berechnung (Neuberechnung des Zellenwerts) wird gestartet, wenn sich eines der Argumente ändert, sodass die Anforderung beispielsweise unmittelbar nach Auswahl der ersten Station und nicht nur durch Klicken auf „Suchen“ gesendet wird. Wir müssen einerseits die Stationen und das Datum in die Argumente der Formel übernehmen, andererseits aber nicht auf ihre Änderung reagieren. In mrr gibt es dafür einen eleganten Mechanismus, der als passives Hören bezeichnet wird.

  results: [getTrains, '-stationFrom', '-stationTo', '-date', 'searchTrains'], 

Fügen Sie einfach ein Minus vor dem Zellennamen hinzu und voila! Jetzt reagieren die Ergebnisse nur noch auf Änderungen an der searchTrains- Zelle.

In diesem Fall fungiert die searchTrains- Zelle als " Zellensignal " und die Zellen von stationFrom und anderen als " Zellwerte ". Für eine Signalzelle ist nur der Moment wichtig, in dem die Werte durch sie „fließen“ und welche Art von Daten es sein wird - jedenfalls: Es kann einfach wahr sein, „1“ oder was auch immer (in unserem Fall sind dies DOM-Ereignisobjekte ) Für eine Wertzelle ist ihr Wert wichtig, aber gleichzeitig sind die Momente ihrer Änderung unbedeutend. Diese beiden Zelltypen schließen sich nicht gegenseitig aus: Viele Zellen sind sowohl Signale als auch Werte. Auf der Syntaxebene in mrr unterscheiden sich diese beiden Zelltypen in keiner Weise, aber das konzeptionelle Verständnis eines solchen Unterschieds ist beim Schreiben von reaktivem Code sehr wichtig.

Streams aufteilen


Eine Aufforderung zur Suche nach Sitzplätzen im Zug kann einige Zeit dauern. Daher müssen wir den Lader anzeigen und im Fehlerfall auch reagieren. Es gibt bereits wenige Versprechen für diesen Standardansatz mit automatischer Auflösung.

 const Tickets = withMrr({ $init: { results: {}, } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['nested', (cb, query) => { cb({ loading: true, error: null, data: null }); getTrains(query.from, query.to, query.date) .then(res => cb('data', res)) .catch(err => cb('error', err)) .finally(() => cb('loading', false)) }, 'searchQuery'], availableTrains: 'results.data', }, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div> <div> { state.results.loading && <div className="loading">...</div> } { state.results.error && <div className="error"> . ,  .   .</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train) => <div />) } </div> } </div> </div>); }); 

Mit dem verschachtelten Operator können Sie Daten in Unterzellen „zerlegen“. Das erste Argument für die Formel ist der Rückruf, mit dem Sie die Daten in die Unterzelle (eine oder mehrere) „verschieben“ können. Jetzt haben wir separate Streams, die für den Fehler, den Status des Versprechens und die empfangenen Daten verantwortlich sind. Der verschachtelte Operator ist ein sehr leistungsfähiges Werkzeug und eines der wenigen Imperative in mrr (wir geben selbst an, in welche Zellen die Daten eingefügt werden sollen). Während der Zusammenführungsoperator mehrere Threads zu einem zusammenführt, teilt verschachtelt den Thread in mehrere Unter-Threads auf und ist somit das Gegenteil.

Das obige Beispiel ist eine Standardmethode zum Arbeiten mit Versprechungen. In mrr wird es als Versprechungsoperator verallgemeinert und ermöglicht es Ihnen, den Code zu verkürzen:

  results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], //     availableTrains: 'results.data', 

Außerdem stellt der Versprechen-Operator sicher, dass nur die Ergebnisse des letzten Versprechens verwendet werden.



Komponente zur Anzeige der verfügbaren Sitze (der Einfachheit halber lehnen wir verschiedene Fahrzeugtypen ab)

 const TrainSeats = withMrr({ selectSeats: [(seatsNumber, { id }) => new Array(Number(seatsNumber)).fill(true).map(() => ({ trainId: id })), '-seatsNumber', '-$props', 'select'], seatsNumber: [() => 0, 'selectSeats'], }, (state, props, $) => <div className="train">  №{ props.num } { props.from } - { props.to }.   : { props.seats || 0 } { props.seats && <div>     : <input type="number" onChange={ $('seatsNumber') } value={ state.seatsNumber || 0 } max={ props.seats } /> <button onClick={ $('select') }></button> </div> } </div>); 

Um auf Requisiten in einer Formel zuzugreifen, können Sie die spezielle $ props-Box abonnieren.

 const Tickets = withMrr({ ... selectedSeats: '*/selectSeats', }, (state, props, $, connectAs) => { ... <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div> } 

Wir verwenden wieder passives Hören, um die Anzahl der ausgewählten Orte zu erfassen, wenn Sie auf die Schaltfläche "Auswählen" klicken. Wir ordnen jede untergeordnete Komponente dem übergeordneten Element mithilfe der Funktion connectAs zu. Der Benutzer kann Sitzplätze in jedem der vorgeschlagenen Züge auswählen, sodass wir die Änderungen in allen untergeordneten Komponenten mit der Maske "*" abhören können.

Aber hier ist das Problem: Der Benutzer kann zuerst in einem Zug und dann in einem anderen Sitzplätze hinzufügen, damit die neuen Daten die vorherigen Daten verarbeiten. Wie kann man Stream-Daten „akkumulieren“? Zu diesem Zweck gibt es einen Verschlussoperator , der zusammen mit verschachtelt und trichter die Basis von mrr bildet (alle anderen sind nichts anderes als syntaktischer Zucker, der auf diesen drei basiert).

  selectedSeats: ['closure', () => { let seats = []; //     return selectedSeats => { seats = [...seats, selectedSeats]; return seats; } }, '*/selectSeats'], 

Wenn Sie zuerst den Abschluss verwenden (auf componentDidMount), wird ein Abschluss erstellt, der die Formel zurückgibt. Sie hat somit Zugriff auf Abschlussvariablen. Auf diese Weise können Sie Daten zwischen Aufrufen auf sichere Weise speichern - ohne in den Abgrund globaler Variablen und des gemeinsam genutzten veränderlichen Status zu geraten. Durch das Schließen können Sie die Funktionalität von Rx-Operatoren wie Scan und anderen implementieren. Diese Methode eignet sich jedoch für schwierige Fälle. Wenn wir nur den Wert einer Variablen speichern müssen, können wir einfach den Link zum vorherigen Wert der Zelle unter dem speziellen Namen "^" verwenden:

  selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^'] 

Jetzt muss der Benutzer für jedes ausgewählte Ticket einen Vor- und Nachnamen eingeben.

 const SeatDetails = withMrr({}, (state, props, $) => { return (<div> { props.trainId } <input name="name" value={ props.name } onChange={ $('setDetails', e => ['name', e.target.value, props.i]) } /> <input name="surname" value={ props.surname } onChange={ $('setDetails', e => ['surname', e.target.value, props.i]) }/> <a href="#" onClick={ $('removeSeat', props.i) }>X</a> </div>); }) const Tickets = withMrr({ $init: { results: {}, selectedSeats: [], } stationFrom: 'selectStationFrom/val', stationTo: 'selectStationTo/val', searchQuery: [(from, to, date) => ({ from, to, date }), '-stationFrom', '-stationTo', '-date', 'searchTrains'], results: ['promise', (query) => getTrains(query.from, query.to, query.date), 'searchQuery'], availableTrains: 'results.data', selectedSeats: [(seats, prev) => [...seats, ...prev], '*/selectSeats', '^'] }, (state, props, $, connectAs) => { return (<div> <div> <OptionsInput { ...connectAs('selectStationFrom') } getOptions={ getMatchedStations } /> - <OptionsInput { ...connectAs('selectStationTo') } getOptions={ getMatchedStations } /> <input type="date" onChange={ $('date') } /> <button onClick={ $('searchTrains') }></button> </div> <div> { state.results.loading && <div className="loading">...</div> } { state.results.error && <div className="error"> . ,  .   .</div> } { state.availableTrains && <div className="results"> { state.availableTrains.map((train, i) => <TrainSeats key={i} {...train} {...connectAs('train' + i)}/>) } </div> } { state.selectedSeats.map((seat, i) => <SeatDetails key={i} i={i} { ...seat } {...connectAs('seat' + i)}/>) } </div> </div>); }); 

Die Zelle selectedSeats enthält ein Array ausgewählter Speicherorte. Wenn der Benutzer den Vor- und Nachnamen für jedes Ticket eingibt, müssen wir die Daten in den entsprechenden Elementen des Arrays ändern.

  selectedSeats: [(seats, details, prev) => { // ??? }, '*/selectSeats', '*/setDetails', '^'] 

Der Standardansatz ist für uns nicht geeignet: In der Formel müssen wir wissen, welche Zelle sich geändert hat, und entsprechend reagieren. Eine der Formen des Zusammenführungsoperators wird uns helfen.

  selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, }, '^'/*,       */], 

Dies ist ein bisschen wie bei Redux-Reduzierern, jedoch mit einer flexibleren und leistungsfähigeren Syntax. Und Sie können keine Angst haben, das Array zu mutieren, da nur die Formel einer Zelle die Kontrolle darüber hat. Parallele Änderungen werden ausgeschlossen (aber das Mutieren von Arrays, die als Argumente übergeben werden, ist es sicherlich nicht wert).

Reaktive Sammlungen


Das Muster, wenn die Zelle das Array speichert und ändert, ist sehr häufig. Es gibt drei Arten von Operationen mit dem Array: Einfügen, Ändern, Löschen. Um dies zu beschreiben, gibt es einen eleganten Coll- Operator. Verwenden Sie diese Option, um die Berechnung von selectedSeats zu vereinfachen.

Es war:

  selectedSeats: ['merge', { '*/selectSeats': (seats, prev) => { return [...prev, ...seats]; }, '*/setDetails': ([field, value, i], prev) => { prev[i][field] = value; return prev; }, '*/removeSeat': (i, prev) => { prev.splice(i, 1); return prev; }, 'addToCart': () => [], }, '^'] 

wurde:

  selectedSeats: ['coll', { create: '*/selectSeats', update: '*/setDetails', delete: ['merge', '*/removeSeat', [() => ({}), 'addToCart']] }] 

Das Datenformat im setDetails-Stream muss jedoch ein wenig geändert werden:

  <input name="name" onChange={ $('setDetails', e => [{ name: e.target.value }, props.i]) } /> <input name="surname" onChange={ $('setDetails', e => [{ surname: e.target.value }, props.i]) }/> 

Mit dem Coll- Operator beschreiben wir drei Threads, die sich auf unser Array auswirken. In diesem Fall muss der Erstellungsstrom die Elemente selbst enthalten, die dem Array hinzugefügt werden sollen (normalerweise Objekte). Der Löschstrom akzeptiert entweder die Indizes der zu löschenden Elemente (sowohl in '* / removeSeat') als auch die Masken. Die Maske {} löscht alle Elemente, und die Maske {name: 'Carl'} löscht beispielsweise alle Elemente mit dem Namen Carl. Der Aktualisierungsdatenstrom akzeptiert Wertepaare: die Änderung, die mit dem Element (Maske oder Funktion) vorgenommen werden muss, und den Index oder die Maske der Elemente, die geändert werden müssen. Beispiel: [{Nachname: 'Johnson'}, {}] setzt den Johnson-Nachnamen auf alle Elemente des Arrays.

Der Coll-Operator verwendet so etwas wie eine interne Abfragesprache, was die Arbeit mit Sammlungen erleichtert und deklarativer macht.

Der vollständige Code unserer Anwendung in JsFiddle.

Wir haben fast alle notwendigen Grundfunktionen von mrr kennengelernt. Ein ziemlich wichtiges Thema, das über Bord geblieben ist, ist das globale Vermögensmanagement, das in zukünftigen Artikeln erörtert werden kann. Jetzt können Sie mrr verwenden, um den Status innerhalb einer Komponente oder einer Gruppe verwandter Komponenten zu verwalten.

Schlussfolgerungen


Was ist die Macht von Herrn?


Mit mrr können Sie Anwendungen in React in einem funktional-reaktiven Stil schreiben (mrr kann als Make React Reactive entschlüsselt werden). mrr ist sehr ausdrucksstark - Sie verbringen weniger Zeit damit, Codezeilen zu schreiben.

mrr bietet eine kleine Reihe grundlegender Abstraktionen, die für alle Fälle ausreichen. In diesem Artikel werden fast alle Hauptfunktionen und -techniken von mrr beschrieben.Es gibt auch Tools zum Erweitern dieses Basissatzes (die Möglichkeit, benutzerdefinierte Operatoren zu erstellen). Sie können schönen deklarativen Code schreiben, ohne Hunderte von Seiten des Handbuchs zu lesen und ohne die theoretischen Tiefen der funktionalen Programmierung zu studieren - es ist unwahrscheinlich, dass Sie beispielsweise Monaden verwenden müssen, weil mrr selbst ist eine riesige Monade, die reine Berechnung von Zustandsmutationen trennt.

Während in anderen Bibliotheken heterogene Ansätze (zwingend unter Verwendung von Methoden und deklarativ unter Verwendung reaktiver Bindemittel) häufig nebeneinander existieren, von denen der Programmierer zufällig „Salat“ mischt, gibt es in mrr eine einzige grundlegende Essenz - einen Strom, der zur Homogenität und Einheitlichkeit des Codes beiträgt. Komfort, Bequemlichkeit, Einfachheit und Zeitersparnis für einen Programmierer sind die Hauptvorteile von mrr (ab hier ist eine weitere Dekodierung von mrr als „mrrrr“, dh das Schnurren einer Katze, die mit dem Leben einer Katze zufrieden ist).

Was sind die Nachteile?


Das Programmieren mit "Zeilen" hat sowohl Vor- als auch Nachteile. Sie können den Namen der Zelle nicht automatisch vervollständigen und nicht nach dem Ort suchen, an dem sie definiert ist. Andererseits gibt es in mrr immer nur einen Ort, an dem das Verhalten der Zelle bestimmt wird, und es ist einfach, es mit einer einfachen Textsuche zu finden, während nach dem Ort gesucht wird, an dem der Wert des Redux-Felds des Geschäfts bestimmt wird, oder noch weniger das Statusfeld, wenn der native setState verwendet wird kann länger sein.

Wer könnte daran interessiert sein?


Zuallererst sind Anhänger der funktionalen Programmierung Menschen, für die der Vorteil eines deklarativen Ansatzes offensichtlich ist. Natürlich gibt es bereits koschere ClojureScript-Lösungen, aber sie bleiben ein Nischenprodukt, während React den Ball regiert. Wenn Ihr Projekt bereits Redux verwendet, können Sie mrr verwenden, um den lokalen Status zu verwalten, und in Zukunft auf global umsteigen. Auch wenn Sie derzeit nicht vorhaben, neue Technologien einzusetzen, können Sie sich mit mrr befassen, um Ihr Gehirn zu „dehnen“, indem Sie vertraute Aufgaben in einem neuen Licht betrachten, da sich mrr erheblich von den gängigen State-Management-Bibliotheken unterscheidet.

Kann das schon benutzt werden?


Im Prinzip ja :) Die Bibliothek ist jung, wurde bisher in mehreren Projekten aktiv genutzt, aber die API der Grundfunktionalität wurde bereits festgelegt. Jetzt wird hauptsächlich an verschiedenen Lotionen (syntaktischer Zucker) gearbeitet, um die Entwicklung weiter zu beschleunigen und zu erleichtern. Übrigens, in den Prinzipien von mrr gibt es nichts Spezifisches für React, es kann für die Verwendung mit jeder Komponentenbibliothek angepasst werden (React wurde aufgrund der fehlenden eingebauten Reaktivität oder einer allgemein akzeptierten Bibliothek dafür ausgewählt).

Vielen Dank für Ihre Aufmerksamkeit, ich bin dankbar für das Feedback und die konstruktive Kritik!

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


All Articles