Wir verwenden zu oft Redux-Selektoren

Wenn ich die Datei {domain} /selectors.js in den großen React / Redux-Projekten betrachte, mit denen ich arbeite, sehe ich oft eine riesige Liste von Redux-Selektoren dieser Art:


getUsers(state) getUser(id)(state) getUserId(id)(state) getUserFirstName(id)(state) getUserLastName(id)(state) getUserEmailSelector(id)(state) getUserFullName(id)(state) … 

Auf den ersten Blick sieht die Verwendung von Selektoren nicht ungewöhnlich aus, aber mit der Erfahrung beginnen wir zu verstehen, dass es zu viele Selektoren geben kann. Und es scheint, wir haben bis zu diesem Punkt überlebt.


Bild

Redux und Selektoren


Schauen wir uns Redux an. Was ist er, warum? Nach dem Lesen von redux.js.org haben wir verstanden, dass Redux ein "vorhersehbarer Container zum Speichern des JavaScript-Anwendungsstatus" ist.


Bei Verwendung von Redux wird empfohlen, Selektoren zu verwenden, auch wenn diese optional sind. Selektoren sind nur Mittel, um einige Teile aus dem gesamten Zustand herauszuholen, d. H. Funktionen der Form (State) => SubState . Normalerweise schreiben wir Selektoren, um nicht direkt auf den Status zuzugreifen, und können dann die Ergebnisse dieser Selektoren kombinieren oder auswendig lernen. Hört sich vernünftig an.


Tief eingetaucht in Selektoren


Die Liste der Selektoren, die ich in der Einleitung zu diesem Artikel zitiert habe, ist charakteristisch für Code, der in Eile erstellt wurde.


Stellen Sie sich vor, wir haben ein Benutzermodell und möchten ihm ein neues E-Mail-Feld hinzufügen. Wir haben eine Komponente, die erwartet firstName , dass firstName und lastName eingegeben werden, und jetzt wartet sie auf eine weitere email . Der Logik im Code mit den Selektoren getUserEmailSelector und ein neues E-Mail-Feld einführend, muss der Autor den Selektor getUserEmailSelector hinzufügen und ihn verwenden, um dieses Feld an die Komponente zu übergeben. Bingo!


Aber ist Bingo? Und wenn wir einen anderen Selektor bekommen, welcher wird komplizierter? Wir werden es mit anderen Selektoren kombinieren und vielleicht kommen wir zu diesem Bild:


 const getUsers = (state) => state.users; const getUser = (id) => (state) => getUsers(state)[id]; const getUserEmailSelector = (id) => (state) => getUser(id)(state).email; 

Die erste Frage stellt sich: Was soll der Selektor getUserEmailSelector zurückgeben, wenn der Selektor getUser zurückgibt? Und dies ist eine wahrscheinliche Situation - Bugs, Refactoring, Legacy - alles kann dazu führen. Im Allgemeinen ist es nie Aufgabe der Selektoren, Fehler zu behandeln oder Standardwerte bereitzustellen.


Das zweite Problem tritt beim Testen solcher Selektoren auf. Wenn wir sie mit Unit-Tests abdecken wollen, benötigen wir Scheindaten, die mit den Daten aus der Produktion identisch sind. Wir müssen die Scheindaten des gesamten Staates (da der Staat in der Produktion nicht inkonsistent sein kann) nur für diesen Selektor verwenden. Abhängig von der Architektur unserer Anwendung kann dies sehr unpraktisch sein: Ziehen von Daten in Tests.


Nehmen wir an, wir haben den getUserEmailSelector Selektor wie oben beschrieben geschrieben und getestet. Wir benutzen es und verbinden die Komponente mit dem Staat:


 const mapStateToProps = (state, ownProps) => ({ firstName: getUserFirstName(ownProps.userId)(state), lastName: getUserLastName(ownProps.userId)(state), //   email: getUserEmailName(ownProps.userId)(state), }) 

Nach der obigen Logik haben wir die Selektoren erhalten, die am Anfang des Artikels standen.
Wir sind zu weit gegangen. Als Ergebnis haben wir eine Pseudo-API für die Entität User geschrieben. Diese API kann nicht außerhalb von Redux verwendet werden, da eine vollständige Statusumwandlung erforderlich ist. Darüber hinaus ist diese API nur schwer zu erweitern. Wenn Sie der Entität "User" neue Felder hinzufügen, müssen Sie neue Selektoren erstellen, sie zu "mapStateToProps" hinzufügen und mehr Code für die Textausgabe schreiben.


Oder sollten Sie direkt auf die Entitätsfelder zugreifen?


Wenn das Problem nur darin besteht, dass wir zu viele Selektoren haben - verwenden wir vielleicht nur getUser und greifen direkt auf die Entitätseigenschaften zu, die wir benötigen?


 const user = getUser(id)(state); const email = user.email; 

Dieser Ansatz löst das Problem des Schreibens und der Unterstützung einer großen Anzahl von Selektoren, schafft jedoch ein anderes Problem. Wenn wir das Benutzermodell ändern müssen, müssen wir auch alle Stellen kontrollieren, an denen user.email ( Anmerkung des Übersetzers oder anderes Feld, das wir ändern). Bei einer großen Menge an Code im Projekt kann dies eine schwierige Aufgabe werden und sogar ein kleines Refactoring erschweren. Wenn wir einen Selektor hatten, schützte er uns vor solchen Konsequenzen von Änderungen, weil übernahm die Verantwortung für die Arbeit mit dem Modell und der Code mit dem Selektor wusste nichts über das Modell.


Direktzugriff ist verständlich. Aber wie sieht es mit dem Empfang berechneter Daten aus? Zum Beispiel mit dem vollständigen Benutzernamen, welcher ist eine Verkettung des Vor- und Nachnamens? Müssen weiter graben ...


Bild

Das Domain-Modell ist überschrieben. Redux - Sekundär


Sie können zu diesem Bild gelangen, indem Sie zwei Fragen beantworten:


  • Wie definieren wir unser Domainmodell?
  • Wie werden wir die Daten speichern? (Statusverwaltung, dafür verwenden wir Redux * Anmerkung des Übersetzers *, dass die Persistenzschicht in DDD aufgerufen wird)

Beantworten wir die Frage „Wie definieren wir das Domain-Modell?“ (In unserem Fall „Benutzer“), abstrahieren wir von Redux und entscheiden, was ein „Benutzer“ ist und welche API für die Interaktion erforderlich ist.


 // api.ts type User = { id: string, firstName: string, lastName: string, email: string, ... } const getFirstName = (user: User) => user.firstName; const getLastName = (user: User) => user.lastName; const getFullName = (user: User) => `${user.firstName} ${user.lastName}`; const getEmail = (user: User) => user.email; ... const createUser = (id: string, firstName: string, ...) => User; 

Es ist gut, wenn wir diese API immer verwenden und das Benutzermodell außerhalb der Datei api.ts als nicht zugänglich betrachten. Dies bedeutet, dass wir uns seitdem niemals direkt den Feldern der Entität zuwenden werden Der Code, der die API verwendet, weiß nicht einmal, welche Entität Felder hat.


Jetzt können wir zu Redux zurückkehren und Probleme lösen, die nur den Status betreffen:


  • Welchen Platz nehmen Nutzer in unserem Artikel ein?
  • Wie sollen wir Benutzer speichern? Eine Liste? Wörterbuch (Schlüsselwert)? Wie sonst
  • Wie erhalten wir eine Benutzerinstanz vom Staat? Sollte eine Notiz verwendet werden? (im Kontext des getUser-Selektors)

Kleine API mit großen Vorteilen


Unter Anwendung des Prinzips der Aufteilung der Verantwortung zwischen Fachgebiet und Staat erhalten wir viele Prämien.


Ein gut dokumentiertes Domänenmodell (Benutzermodell und dessen API) in der Datei api.ts. Es eignet sich gut zum Testen, als hat keine Abhängigkeiten. Wir können das Modell und die API zur Wiederverwendung in anderen Anwendungen in die Bibliothek extrahieren.


Wir können API-Funktionen einfach als Selektoren kombinieren, was einen unvergleichlichen Vorteil gegenüber dem direkten Zugriff auf Eigenschaften darstellt. Darüber hinaus ist unsere Datenschnittstelle jetzt einfach zu warten - wir können das Benutzermodell problemlos ändern, ohne den Code zu ändern, in dem es verwendet wird.


Mit der API ist keine Zauberei passiert, es sieht immer noch klar aus. Die API ähnelt dem, was mit Selektoren gemacht wurde, hat aber einen entscheidenden Unterschied: Sie benötigt nicht den gesamten Status, muss nicht mehr den vollständigen Status der Anwendung zum Testen unterstützen - die API hat nichts mit Redux und ihrem Code zu tun.


Die Komponentenstützen sind sauberer geworden. Anstatt auf die Eingabe der Eigenschaften Vorname, Nachname und E-Mail zu warten, empfängt die Komponente eine Benutzerinstanz und verwendet intern ihre API, um auf die erforderlichen Daten zuzugreifen. Es stellt sich heraus, dass wir nur einen Selektor benötigen - getUser.


Eine solche API bietet Reduzierern und Middleware Vorteile. Der Vorteil besteht im Wesentlichen darin, dass Sie zuerst eine Instanz von User abrufen, die darin enthaltenen fehlenden Werte beheben, alle Fehler verarbeiten oder verhindern und dann die API-Methoden verwenden können. Dies ist besser, als jedes einzelne Feld mit Hilfe von Selektoren vom Themenbereich isoliert darzustellen. Somit wird Redux wirklich zu einem „vorhersehbaren Container“ und hört auf, ein „göttliches“ Objekt mit Wissen über alles zu sein.


Fazit


Mit guten Absichten (lesen Sie hier - Selektoren) ist der Weg zur Hölle geebnet: Wir wollten nicht direkt auf die Felder der Entität zugreifen und haben dafür separate Selektoren gemacht.


Obwohl die Idee der Selektoren an sich gut ist, erschwert ihre Überbeanspruchung die Pflege unseres Codes.


Die im Artikel beschriebene Lösung schlägt vor, das Problem in zwei Schritten zu lösen: Beschreiben Sie zuerst das Domain-Modell und dessen API und beschäftigen Sie sich dann mit Redux (Datenspeicherung, Selektoren). Auf diese Weise schreiben Sie besseren und kleineren Code - Sie benötigen nur einen Selektor, um eine flexiblere und skalierbarere API zu erstellen.


Hinweise für Übersetzer


  1. Ich habe das Wort state verwendet, da es anscheinend fest im Vokabular der russischsprachigen Entwickler verankert ist.
  2. Der Autor verwendet die Wörter Upstream / Downstream, um "High-Level- / Low-Level-Code" (falls nach Martin) oder "Code, der unten verwendet wird / Code, der unten das verwendet, was oben geschrieben ist" zu bedeuten, aber es ist nicht korrekt, herauszufinden, wie dies in der Übersetzung verwendet wird Ich könnte mich deshalb trösten, indem ich versuche, den allgemeinen Sinn nicht zu verärgern.

Gerne nehme ich Kommentare und Korrekturvorschläge in PM entgegen und korrigiere sie.

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


All Articles