So organisieren Sie den allgemeinen Status in Reaktionsanwendungen ohne Verwendung von Bibliotheken (und warum wird Mobx benötigt)

Sofort ein kleiner Spoiler - das Organisieren eines Staates in Mobx unterscheidet sich nicht vom Organisieren eines allgemeinen Staates ohne Mobx in einer reinen Reaktion. Die Antwort auf die natürliche Frage lautet: Warum wird dieser Mobx dann tatsächlich benötigt? Sie finden ihn am Ende des Artikels. In der Zwischenzeit konzentriert sich der Artikel auf die Organisation des Zustands in einer sauberen Reaktionsanwendung ohne externe Bibliotheken.




Die Reaktion bietet eine Möglichkeit zum Speichern und Aktualisieren des Status von Komponenten mithilfe der state-Eigenschaft für eine Instanz einer Klassenkomponente und der setState-Methode. In der Reaktionsgemeinschaft werden jedoch eine Reihe zusätzlicher Bibliotheken und Ansätze für die Arbeit mit dem Staat verwendet (Flussmittel, Redux, Reduxionen, Effektor, Mobx, Gehirn, eine Reihe von Bibliotheken). Aber ist es möglich, eine ausreichend große Anwendung mit einer Reihe von Geschäftslogiken mit einer großen Anzahl von Entitäten und komplexen Datenbeziehungen zwischen Komponenten nur mit setState zu erstellen? Sind zusätzliche Bibliotheken erforderlich, um mit dem Status zu arbeiten? Lass es uns herausfinden.

Wir haben also setState, das den Status aktualisiert und den Renderer der Komponente aufruft. Was aber, wenn viele Komponenten, die nicht miteinander verbunden sind, dieselben Daten benötigen? Im offiziellen Dock der Reaktion gibt es einen Abschnitt "Aufheben des Zustands" mit einer detaillierten Beschreibung - wir erheben den Zustand einfach auf den Vorfahren, der diesen Komponenten gemeinsam ist, und übergeben Daten und Funktionen, um ihn durch Requisiten (und gegebenenfalls durch Zwischenkomponenten) zu ändern. Für kleine Beispiele sieht dies vernünftig aus, aber die Realität ist, dass in komplexen Anwendungen viele Abhängigkeiten zwischen Komponenten bestehen können und die Tendenz, Zustände auf eine gemeinsame Komponente des Vorfahren zu übertragen, dazu führt, dass der gesamte Zustand immer höher wird und zusammen mit ihm in der Stammkomponente der App landet die Logik zum Aktualisieren dieses Status für alle Komponenten. Infolgedessen wird setState nur ausgeführt, um die Datenkomponente lokal oder in der Stammkomponente der App zu aktualisieren, in der die gesamte Logik konzentriert wird.


Aber ist es möglich, den Prozess- und Renderstatus in einer Reaktionsanwendung zu speichern, ohne setState oder zusätzliche Bibliotheken zu verwenden und von beliebigen Komponenten aus allgemeinen Zugriff auf diese Daten zu gewähren?


Die gebräuchlichsten Javascript-Objekte und bestimmte Regeln für deren Organisation helfen uns dabei.


Zunächst müssen Sie jedoch lernen, wie Sie Anwendungen in Entitätstypen und deren Beziehungen zerlegen.


Zunächst führen wir ein Objekt ein, das globale Daten, die für die gesamte Anwendung gelten, (dies können die Einstellungen für Stile, Lokalisierung, Fenstergrößen usw. sein) in einem einzelnen AppState-Objekt speichert und dieses Objekt einfach in eine separate Datei einfügt.


// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... } 

Jetzt können Sie in jeder Komponente die Daten unseres Geschäfts importieren und verwenden.


 import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> ) 

Wir gehen noch weiter - fast jede Anwendung hat die Essenz des aktuellen Benutzers (es spielt keine Rolle, wie sie erstellt wird oder vom Server stammt usw.), sodass sich das Singleton-Objekt unseres Benutzers auch im Status der Anwendung befindet. Es kann auch in eine separate Datei verschoben und auch importiert werden, oder es kann sofort im AppState-Objekt gespeichert werden. Und jetzt die Hauptsache - Sie müssen das Diagramm der Entitäten bestimmen, aus denen die Anwendung besteht. In Bezug auf eine Datenbank sind dies Tabellen mit Eins-zu-Viele- oder Viele-zu-Viele-Beziehungen, und diese gesamte Beziehungskette beginnt mit der Hauptessenz des Benutzers. In unserem Fall speichert das Objekt des Benutzers einfach ein Array anderer Objekt-Entitäts-Speicher, wobei jeder Objekt-Speicher wiederum Arrays anderer Entitäts-Speicher speichert.


Hier ist ein Beispiel: Es gibt eine Geschäftslogik, die ausgedrückt wird als "Der Benutzer kann Ordner, Projekte in jedem Ordner, in jedem Aufgabenprojekt und in jeder Unteraufgabenaufgabe erstellen / bearbeiten / löschen" (es stellt sich wie ein Task-Manager heraus) und wird im Statusdiagramm angezeigt ungefähr so:


 export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } } 

Jetzt kann die Stammkomponente der App dieses Objekt einfach importieren und einige Informationen über den Benutzer rendern. Anschließend kann das Benutzerobjekt auf die Dashboard-Komponente übertragen werden


  .... <Dashboard user={appState.user}/> .... 

und er kann die Liste der Ordner rendern


  ... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ... 

und jede Komponente des Ordners zeigt eine Liste von Projekten an


  .... <div>{folder.projects.map(project=><Project project={project}/>)}</div> .... 

und jede Komponente des Projekts kann Aufgaben auflisten


  .... <div>{project.tasks.map(task=><Task task={task}/>)}</div> .... 

und schließlich kann jede Aufgabenkomponente eine Liste von Unteraufgaben rendern, indem das gewünschte Objekt an die Unteraufgabenkomponente übergeben wird


  .... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> .... 

Natürlich zeigt auf einer Seite niemand alle Aufgaben aller Projekte aller Ordner an, sie werden durch Seitenbereiche (z. B. eine Liste von Ordnern), durch Seiten usw. unterteilt, aber die allgemeine Struktur ist ungefähr dieselbe - die übergeordnete Komponente rendert die eingebettete Komponente, die ein Objekt mit Requisiten übergibt Daten. Ein wichtiger Punkt sollte beachtet werden: Jedes Objekt (z. B. ein Objekt eines Ordners, eines Projekts oder einer Aufgabe) wird nicht im Status einer Komponente gespeichert. Die Komponente empfängt es einfach über Requisiten als Teil eines allgemeineren Objekts. Wenn die Projektkomponente beispielsweise das Aufgabenobjekt ( <div>{project.tasks.map(task=><Task task={task}/>)}</div> ) an die untergeordnete Komponente von Task übergibt, da die Objekte in einem einzelnen Objekt gespeichert sind Sie können dieses Aufgabenobjekt jederzeit von außen ändern, z. B. AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "bearbeitete Aufgabe", und dann die Stammkomponente aktualisieren (ReactDOM.render (<App />) ) und auf diese Weise erhalten wir den aktuellen Status der Anwendung.


Angenommen, wir möchten eine neue Unteraufgabe erstellen, wenn wir in der Task-Komponente auf die Schaltfläche "+" klicken. Alles ist einfach


  onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() } 

Da die Task-Komponente das Task-Objekt als Requisiten empfängt und dieses Objekt nicht in seinem Status gespeichert wird, sondern Teil des globalen AppState-Speichers ist (dh das Task-Objekt wird im Task-Array des allgemeineren Projektobjekts gespeichert und ist wiederum Teil des Benutzerobjekts und der Benutzer ist bereits im AppState gespeichert ) und dank dieser Konnektivität können Sie nach dem Hinzufügen eines neuen Task-Objekts zum Subtasks-Array das Root-Komponenten-Update aufrufen und dadurch das Haus für alle Datenänderungen (unabhängig davon, wo sie stattgefunden haben) aktualisieren und aktualisieren, indem Sie einfach die Upd-Funktion aufrufen ateDOM, das wiederum einfach die Stammkomponente aktualisiert.


 export function updateDOM(){ ReactDom.render(<App/>, rootElement); } 

Dabei spielt es keine Rolle, welche Daten von welchen Teilen von AppState und von welchen Stellen wir ändern (Sie können beispielsweise ein Ordnerobjekt über Requisiten über zwischengeschaltete Projekt- und Aufgabenkomponenten an die Subtask-Komponente weiterleiten und einfach den Ordnernamen aktualisieren (this.props.folder.name = "neuer Name) ") - Aufgrund der Tatsache, dass Komponenten Daten über Requisiten empfangen, werden durch die Aktualisierung der Stammkomponente alle verschachtelten Komponenten und die gesamte Anwendung aktualisiert.


Lassen Sie uns nun versuchen, die Arbeit mit der Seite etwas komfortabler zu gestalten. Im obigen Beispiel können Sie feststellen, dass jedes Mal ein neues Entitätsobjekt erstellt wird (z. B. project.tasks.push({text: "", subtasks: [], ...}) wenn das Objekt viele Eigenschaften mit Standardparametern aufweist Um sie aufzulisten, können Sie einen Fehler machen und etwas vergessen usw. Das erste, was Ihnen in den Sinn kommt, ist, die Erstellung eines Objekts in eine Funktion zu integrieren, in der Standardfelder zugewiesen werden, und sie gleichzeitig mit neuen Daten neu zu definieren


 function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } } 

Wenn Sie jedoch von der anderen Seite schauen, ist diese Funktion der Konstruktor einer bestimmten Entität, und Javascript-Klassen eignen sich hervorragend für diese Rolle


 class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } } 

Wenn Sie dann das Objekt erstellen, wird einfach eine Instanz der Klasse erstellt, mit der einige Standardfelder überschrieben werden können


 onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) } 

Außerdem können Sie feststellen, dass beim Erstellen von Klassen für Projektobjekte, Benutzer und Unteraufgaben auf dieselbe Weise Code-Duplikate im Konstruktor erstellt werden


 constructor(){ Object.assign(this,data) } 

Wir können jedoch die Vererbung nutzen und diesen Code in den Konstruktor der Basisklasse ziehen.


 class BaseStore { constructor(data){ Object.update(this, data); } } 

Außerdem werden Sie feststellen, dass wir jedes Mal, wenn wir einen Status aktualisieren, die Felder des Objekts manuell ändern


 user.firstName = "..."; user.lastName = "..."; updateDOM(); 

und es wird schwierig zu verfolgen, zu verhandeln und zu verstehen, was in der Komponente geschieht, und daher muss ein gemeinsamer Kanal festgelegt werden, über den Aktualisierungen von Daten durchlaufen werden, und dann können wir Protokollierung und alle möglichen anderen Annehmlichkeiten hinzufügen. Zu diesem Zweck besteht die Lösung darin, eine Aktualisierungsmethode in der Klasse zu erstellen, die ein temporäres Objekt mit neuen Daten verwendet und sich selbst aktualisiert und die Regel festlegt, dass Objekte nur über die Aktualisierungsmethode und nicht durch direkte Zuweisung aktualisiert werden können


 class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } } //// user.update({firstName: "...", lastName: "..."}) 

Um den Code nicht in jeder Klasse zu duplizieren, verschieben wir diese Aktualisierungsmethode auch in die Basisklasse.


Jetzt können Sie sehen, dass wir beim Aktualisieren einiger Daten die updateDOM () -Methode manuell aufrufen müssen. Es ist jedoch praktisch, dieses Update jedes Mal automatisch durchzuführen, wenn die Update-Methode ({...}) der Basisklasse aufgerufen wird.
Es stellt sich heraus, dass die Basisklasse ungefähr so ​​aussehen wird


 class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } } 

Damit beim sukzessiven Aufruf der update () -Methode keine unnötigen Aktualisierungen auftreten, können Sie die Aktualisierung der Komponente auf den nächsten Ereigniszyklus verzögern


 let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } } 

Darüber hinaus können Sie die Funktionalität der Basisklasse schrittweise erhöhen. Um beispielsweise nicht jedes Mal manuell eine Anforderung an den Server senden zu müssen, können Sie zusätzlich zur Aktualisierung des Status im Hintergrund eine Anforderung an die Aktualisierungsmethode ({..}) senden. Sie können einen Live-Aktualisierungskanal für Web-Sockets organisieren, indem Sie ein Konto für jedes erstellte Objekt in der globalen Hash-Map hinzufügen, ohne die Komponenten zu ändern und in irgendeiner Weise mit Daten zu arbeiten.


Es gibt noch viel zu tun, aber ich möchte ein interessantes Thema erwähnen - sehr oft ein Objekt mit Daten an die erforderliche Komponente übergeben (z. B. wenn eine Projektkomponente eine Aufgabenkomponente rendert).


 <div>{project.tasks.map(task=><Task task={task}/>)}</div> 

Die eigentliche Komponente der Aufgabe benötigt möglicherweise einige Informationen, die nicht direkt in der Aufgabe gespeichert sind, sondern sich im übergeordneten Objekt befinden.


Angenommen, Sie möchten alle Aufgaben in einer Farbe färben, die im Projekt gespeichert ist und allen Aufgaben gemeinsam ist. Dazu muss die Projektkomponente zusätzlich zu den Task-Requisiten auch ihre Projekt-Requisiten <Task task={task} project={this.props.project}/> . Wenn Sie die Aufgabe plötzlich in einer Farbe färben müssen, die allen Aufgaben in einem Ordner gemeinsam ist, müssen Sie das aktuelle Ordnerobjekt von der Ordnerkomponente zur Aufgabenkomponente übertragen, indem Sie es über die Zwischenkomponente Projekt weiterleiten.
Eine fragile Abhängigkeit scheint zu bestehen, dass die Komponente wissen sollte, was ihre verschachtelten Komponenten erfordern. Darüber hinaus erfordert die Möglichkeit eines Reaktionskontexts, obwohl dies die Übertragung durch Zwischenkomponenten vereinfacht, immer noch eine Beschreibung des Anbieters und das Wissen darüber, welche Daten für die untergeordneten Komponenten benötigt werden.


Das Hauptproblem besteht jedoch darin, dass Sie jedes Mal, wenn Sie ein Design bearbeiten oder die Wunschliste eines Kunden ändern, wenn eine Komponente neue Informationen benötigt, die höheren Komponenten ändern müssen, indem Sie entweder Requisiten weiterleiten oder Kontextanbieter erstellen. Ich möchte, dass die Komponente, die über Requisiten ein Objekt mit Daten empfängt, irgendwie auf einen Teil unseres Anwendungsstatus zugreift. Und hier passt Javascript gut (im Gegensatz zu funktionalen Sprachen wie Ulme oder unveränderlichen Ansätzen wie Redux), sodass Objekte kreisförmige Links zueinander speichern können. In diesem Fall sollte das Task-Objekt ein task.project-Feld mit einem Link zum Objekt des übergeordneten Projekts haben, in dem es gespeichert ist, und das Projektobjekt sollte wiederum einen Link zum Ordnerobjekt usw. zum Root-AppState-Objekt haben. Somit kann die Komponente, egal wie tief sie ist, immer die übergeordneten Objekte über den Link durchlaufen und alle erforderlichen Informationen abrufen, ohne sie durch eine Reihe von Zwischenkomponenten werfen zu müssen. Daher führen wir eine Regel ein: Jedes Mal, wenn Sie ein Objekt erstellen, müssen Sie dem übergeordneten Objekt einen Link hinzufügen. Das Erstellen einer neuen Aufgabe sieht beispielsweise so aus


  ... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask); 

Mit zunehmender Geschäftslogik können Sie außerdem feststellen, dass die Bolterplate mit der Backlink-Unterstützung verbunden ist (z. B. beim Zuweisen eines Links zum übergeordneten Objekt beim Erstellen eines neuen Objekts oder beim Übertragen eines Projekts von einem Ordner in einen anderen müssen Sie nicht nur die Eigenschaft project.folder = newFolder aktualisieren und löschen Sie selbst aus dem Projektarray des vorherigen Ordners und das Hinzufügen eines neuen Ordners zum Projektarray beginnt sich zu wiederholen. Es kann auch in die Basisklasse verschoben werden, sodass beim Erstellen des Objekts die übergeordnete new Task({project: this.porps.project}) new Task({project: this.porps.project}) und die Basisklasse würden automatisch ein neues Objekt zum Array project.tasks hinzufügen. Auch beim Übertragen der Aufgabe in ein anderes Projekt würde es ausreichen, nur das task.update({project: newProject}) zu aktualisieren, und die Basisklasse würde die Aufgabe automatisch löschen Eine Reihe von Aufgaben aus dem vorherigen Projekt, die einem neuen hinzugefügt wurden. Dies erfordert jedoch bereits die Deklaration von Beziehungen (z. B. in statischen Eigenschaften oder Methoden), damit die Basisklasse weiß, welche Felder aktualisiert werden müssen.


Fazit


Auf solch einfache Weise, bei der nur js-Objekte verwendet wurden, kamen wir zu dem Schluss, dass Sie alle Vorteile des Arbeitens mit dem allgemeinen Status der Anwendung erhalten können, ohne die Abhängigkeit von einer externen Bibliothek für die Arbeit mit dem Status in die Anwendung einzuführen.


Die Frage ist, warum wir dann Bibliotheken für die Verwaltung des Staates und insbesondere von Mobx benötigen.


Tatsache ist, dass bei dem beschriebenen Ansatz zur Organisation des allgemeinen Zustands bei Verwendung gewöhnlicher nativer "Vanilla" -J-Objekte (oder Klassenobjekte) ein großer Nachteil besteht: Wenn sich ein kleiner Teil des Zustands oder sogar ein Feld ändert, werden die Komponenten aktualisiert oder "gerendert", die in keiner Weise verbunden sind und hängen nicht von diesem Teil des Staates ab.
Bei großen Anwendungen mit fettgedruckter Benutzeroberfläche führt dies zu Bremsen, da die Reaktion einfach keine Zeit hat, das virtuelle Haus der gesamten Anwendung rekursiv zu vergleichen, da zusätzlich zum Vergleich jedes Renderers jedes Mal ein neuer Objektbaum generiert wird, der das Layout absolut aller Komponenten beschreibt.


Trotz der Bedeutung ist dieses Problem rein technischer Natur - es gibt Bibliotheken, die der Vitual Dom-Reaktion ähneln und den Renderer besser optimieren und das Komponentenlimit erhöhen können.


Es gibt effektivere Hausrenovierungstechniken als das Erstellen eines neuen virtuellen Hausbaums und den anschließenden rekursiven Vergleichsdurchlauf mit dem vorherigen Baum.


Und schließlich gibt es Bibliotheken, die versuchen, das Problem langsamer Aktualisierungen durch einen anderen Ansatz zu lösen - nämlich zu verfolgen, welche Teile des Status mit welchen Komponenten verbunden sind, und beim Ändern einiger Daten nur die Komponenten zu berechnen und zu aktualisieren, die von diesen Daten abhängen, und die verbleibenden Komponenten nicht zu berühren. Redux ist ebenfalls eine solche Bibliothek, erfordert jedoch einen völlig anderen Ansatz für die staatliche Organisation. Im Gegensatz dazu bringt die Mobx-Bibliothek nichts Neues und wir können den Renderer praktisch beschleunigen, ohne etwas in der Anwendung zu ändern. @observable einfach den @observable Dekorator zu den Feldern der Klasse und den @observable Dekorator zu den Komponenten hinzu, die diese Felder rendern, und es bleibt Wenn Sie nur den unnötigen Aktualisierungscode für die Stammkomponente in der update () -Methode unserer Basisklasse ausschneiden, erhalten Sie eine voll funktionsfähige Anwendung. Wenn Sie jedoch jetzt einen Teil des Status oder sogar ein Feld ändern, werden nur diese Komponenten aktualisiert die gereift AK (einlenkenden Methode () überträgt) für ein bestimmtes Feld eines bestimmten Zustands des Objekts.

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


All Articles