
Worum geht es?
Wir werden über mehrere (fünf, um genau zu sein) Methoden, Tricks und blutige Opfer für den Gott des Unternehmens sprechen, die uns dabei zu helfen scheinen, präziseren und aussagekräftigeren Code in unsere Redux- (und NGRX-!) Anwendungen zu schreiben. Die Wege werden von Schweiß und Kaffee geplagt. Bitte treten und stark kritisieren. Wir werden lernen, gemeinsam besser zu codieren.
Ehrlich gesagt wollte ich der Welt zunächst nur von meiner neuen Mikrobibliothek (35 Codezeilen!) Flux-Action-Klasse erzählen, aber angesichts der ständig wachsenden Anzahl von Ausrufen, dass Habr bald zu Twitter wird, und zum größten Teil Ich stimmte ihnen zu und beschloss, etwas ausführlicher zu lesen. Wir haben also 5 Möglichkeiten, um Ihre Redux-Anwendung zu aktualisieren!
Boilerplate kommen heraus
Betrachten Sie ein typisches Beispiel für das Senden einer AJAX-Anfrage an Redux. Stellen wir uns vor, wir brauchen wirklich eine Liste der Siegel vom Server.
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
Wenn Sie nicht genau verstehen, warum hier Fabriken für Selektoren benötigt werden, können Sie hier darüber lesen .
Ich betrachte Nebenwirkungen hier nicht wissentlich. Dies ist ein Thema für einen separaten Artikel voller Wut im Teenageralter und Kritik am bestehenden Ökosystem: D.
Dieser Code enthält mehrere Schwachstellen:
- Aktionsfabriken sind für sich genommen einzigartig, aber wir verwenden immer noch Aktionstypen.
- Wenn neue Entitäten hinzugefügt werden, duplizieren wir weiterhin dieselbe Logik, um das
loading
zu setzen. Die Daten, die wir in data
speichern, und ihre Form können von Anforderung zu Anforderung erheblich variieren, aber die Download-Anzeige ( loading
) bleibt unverändert. - Die Laufzeit des Schalters beträgt O (n) ( fast ). Dies ist an sich kein sehr starkes Argument, da es bei Redux im Prinzip nicht um Leistung geht. Es macht mich mehr wütend, dass Sie für jeden
case
ein paar zusätzliche Zeilen Serving-Code schreiben müssen und dass ein switch
nicht einfach und schön in mehrere aufgeteilt werden kann. - Müssen wir den Fehlerstatus wirklich für jede Entität separat speichern?
- Selektoren sind cool. Gespeicherte Selektoren sind doppelt cool. Sie geben uns eine Abstraktion über unsere Seite, so dass wir später nicht die Hälfte der Anwendung wiederholen müssen, wenn wir ihre Form ändern. Wir ändern nur den Selektor selbst. Was für das Auge nicht angenehm ist, ist eine Reihe primitiver Fabriken, die nur aufgrund der Besonderheiten der Memoisierung bei der Wiederwahl benötigt werden .
Methode 1: Aktionstypen loswerden
Nun, nicht wirklich. Wir lassen JS sie einfach für uns erstellen.
Lassen Sie uns eine Sekunde darüber nachdenken, warum wir im Allgemeinen Aktionstypen benötigen. Nun, natürlich, um den gewünschten Zweig der Logik in unserem Reduzierer zu starten und den Status der Anwendung entsprechend zu ändern. Die eigentliche Frage ist, muss ein Typ eine Zeichenfolge sein? Aber was wäre, wenn wir Klassen verwenden und nach Typ switch
?
class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } }
Alles scheint großartig zu sein, aber es gibt ein Problem: Wir haben die Serialisierung unserer Aktionen verloren. Dies sind keine einfachen Objekte mehr, die wir in einen String konvertieren können und umgekehrt. Jetzt verlassen wir uns auf die Tatsache, dass jede Aktion einen eigenen Prototyp hat, der es tatsächlich ermöglicht, dass ein solches Design wie das switch
von action.constructor
funktioniert. Weißt du, ich mag die Idee wirklich, meine Aktionen in eine Zeichenfolge zu serialisieren und sie zusammen mit einem Fehlerbericht zu senden, und ich bin nicht bereit, sie abzulehnen.
Daher sollte jede Aktion ein Typfeld haben ( hier können Sie sehen, was jede Aktion, die die Aktion respektiert, sonst noch haben sollte). Glücklicherweise hat jede Klasse einen Namen, der einer Zeichenfolge ähnelt. Fügen wir jeder Klasse einen Getter- type
den Namen dieser Klasse zurückgibt.
class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, }
Es funktioniert sogar, aber ich möchte jedem Typ ein Präfix hinzufügen, wie Herr Eric in ducks-modular-redux vorschlägt (ich empfehle, die Gabel der Re-Enten zu betrachten , die für mich noch cooler ist). Um ein Präfix hinzuzufügen, müssen wir die direkte Verwendung des Klassennamens beenden und einen weiteren Getter hinzufügen. Jetzt statisch.
class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, }
Lassen Sie uns das Ganze ein bisschen kämmen. Reduzieren Sie das Kopieren und Einfügen auf ein Minimum und fügen Sie eine weitere Bedingung hinzu: Wenn die Aktion einen Fehler aufweist, muss ihre payload
vom Typ Error
.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
Zu diesem Zeitpunkt funktioniert dieser Code gut mit NGRX, aber Redux kann ihn nicht kauen. Er schwört, dass Handlung einfache Objekte sein sollten. Glücklicherweise können wir mit JS fast alles vom Designer zurückgeben, aber wir brauchen nach dem Erstellen der Aktion wirklich keine Prototypenkette.
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
Basierend auf den obigen Überlegungen wurde die Mikrobibliothek der Flussaktionsklasse geschrieben. Es gibt Tests, 100% Testabdeckung und fast dieselbe ActionStandard
Klasse ActionStandard
mit Generika für TypeScript-Anforderungen ausgestattet ist. Funktioniert sowohl mit TypeScript als auch mit JavaScript.
Methode 2: Wir haben keine Angst, CombineReducers zu verwenden
Die Idee ist einfach zu blamieren: Verwenden Sie combinReducers nicht nur für Reduzierer der obersten Ebene, sondern auch, um die Logik weiter aufzuschlüsseln und einen separaten Reduzierer zum loading
erstellen.
const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, })
Methode 3: Entfernen Sie den Schalter
Und wieder eine äußerst einfache Idee: Verwenden Sie anstelle des switch-case
ein Objekt, aus dem Sie das gewünschte Feld per Schlüssel auswählen können. Der Zugriff auf das Feld des Objekts per Schlüssel ist O (1) und sieht meiner bescheidenen Meinung nach etwas sauberer aus.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
Lassen Sie uns reducerLoading
umgestalten. reducerLoading
wir nun Karten (Objekte) für Reduzierer kennen, können wir diese Karte von reducerLoading
, anstatt einen gesamten Reduzierer zurückzugeben. Dies eröffnet möglicherweise unbegrenzten Spielraum für die Erweiterung der Funktionalität.
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => {
Die offizielle Dokumentation zu Redux spricht ebenfalls über diesen Ansatz. Aus unbekannten Gründen sehe ich jedoch weiterhin viele Projekte, die switch-case
. Basierend auf dem Code aus der offiziellen Dokumentation hat Herr Moshe eine Bibliothek für createReducer
für uns createReducer
.
Methode 4: Verwenden Sie den globalen Fehlerbehandler
Wir müssen den Fehler nicht für jede Entität separat aufbewahren. In den meisten Fällen wollen wir nur den Dialog zeigen. Der gleiche Dialog mit dynamischem Text für alle Entitäten.
Erstellen Sie einen globalen Fehlerbehandler. Im einfachsten Fall könnte es so aussehen:
class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, })
In unserem Nebeneffekt senden wir dann die Aktion ErrorInit
im catch
. Bei Verwendung von Redux-Thunk könnte es ungefähr so aussehen:
const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } }
Jetzt können wir das CatsGetError
in unserem Cat Store CatsGetError
und CatsGetError
nur verwenden, um das CatsGetError
zu wechseln.
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) })
Methode 5: Vor dem Auswendiglernen nachdenken
Schauen wir uns noch einmal einen Stapel Fabriken für Selektoren an.
Ich habe makeSelectorCatsError
weil es nicht mehr benötigt wird, wie wir im vorherigen Kapitel gefunden haben.
const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, )
Warum brauchen wir hier gespeicherte Selektoren? Was genau versuchen wir uns zu merken? Der Zugriff auf das Objektfeld per Schlüssel, was hier geschieht, ist O (1). Wir können gewöhnliche nicht gespeicherte Funktionen verwenden. Verwenden Sie Memoization nur, wenn Sie Daten aus dem Speicher ändern möchten, bevor Sie sie der Komponente übergeben.
const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
Das Auswendiglernen ist sinnvoll, wenn das Ergebnis im laufenden Betrieb berechnet wird. Stellen Sie sich für das folgende Beispiel vor, dass jede Katze ein Objekt mit dem Namensfeld ist und wir eine Zeichenfolge erhalten möchten, die die Namen aller Katzen enthält.
const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), )
Fazit
Mal sehen, wo wir angefangen haben:
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
Und was kam dazu:
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
Ich hoffe, Sie haben keine Zeit umsonst verschwendet, und der Artikel war zumindest ein wenig nützlich für Sie. Wie ich ganz am Anfang sagte, treten Sie bitte und kritisieren Sie hart. Wir werden lernen, gemeinsam besser zu codieren.