Redux Toolkit als Werkzeug für eine effektive Redux-Entwicklung

Bild
Derzeit wird der Löwenanteil der auf dem React-Framework basierenden Webanwendungen mithilfe der Redux-Bibliothek entwickelt. Diese Bibliothek ist die beliebteste Implementierung der FLUX-Architektur und weist trotz einer Reihe offensichtlicher Vorteile erhebliche Nachteile auf, wie z.


  • die Komplexität und „Ausführlichkeit“ der empfohlenen Muster für das Schreiben und Organisieren von Code, die eine große Anzahl von Boilerplates mit sich bringen;
  • Das Fehlen integrierter Steuerelemente für asynchrones Verhalten und Nebenwirkungen führt dazu, dass Sie das richtige Tool aus einer Vielzahl von Add-Ons auswählen müssen, die von Drittanbieter-Entwicklern geschrieben wurden.

Um diese Mängel zu beheben, haben die Redux-Entwickler die Redux Toolkit-Bibliothek eingeführt. Dieses Tool enthält eine Reihe praktischer Lösungen und Methoden zur Vereinfachung der Anwendungsentwicklung mit Redux. Die Entwickler dieser Bibliothek wollten typische Fälle der Verwendung von Redux vereinfachen. Dieses Tool ist keine universelle Lösung für alle möglichen Fälle der Verwendung von Redux, aber Sie können den Code, den der Entwickler schreiben muss, vereinfachen.


In diesem Artikel werden die wichtigsten im Redux Toolkit enthaltenen Tools beschrieben und anhand eines Beispiels für ein Fragment unserer internen Anwendung gezeigt, wie sie in vorhandenem Code verwendet werden.


Kurz über die Bibliothek


Zusammenfassung des Redux Toolkit:


  • vor der Veröffentlichung hieß die Bibliothek Redux-Starter-Kit;
  • die Veröffentlichung fand Ende Oktober 2019 statt;
  • Die Bibliothek wird offiziell von Redux-Entwicklern unterstützt.

Laut den Entwicklern führt das Redux Toolkit die folgenden Funktionen aus:


  • Hilft Ihnen, schnell mit Redux zu beginnen.
  • vereinfacht die Arbeit mit typischen Aufgaben und Redux-Code;
  • Ermöglicht die Verwendung der Best Practices von Redux.
  • bietet Lösungen, die das Misstrauen gegenüber Kesselplatten verringern.

Redux Toolkit bietet eine Reihe von speziell entwickelten und eine Reihe von bewährten Tools , die üblicherweise in Verbindung mit Redux verwendet werden. Mit diesem Ansatz kann der Entwickler entscheiden, wie und welche Tools in seiner Anwendung verwendet werden sollen. Im Verlauf dieses Artikels werden wir feststellen, welche Ausleihen diese Bibliothek verwendet. Weitere Informationen und Abhängigkeiten des Redux Toolkit finden Sie in der Paketbeschreibung @ reduxjs / toolkit .


Die wichtigsten Funktionen der Redux Toolkit-Bibliothek sind:


  • #configureStore - eine Funktion zur Vereinfachung des Erstellens und Konfigurierens von Speicher;
  • #createReducer - eine Funktion, mit der ein Reduzierer kurz und klar beschrieben und erstellt werden kann;
  • #createAction - Gibt die Funktion des Erstellers der Aktion für die angegebene Zeichenfolge des Aktionstyps zurück.
  • #createSlice - kombiniert die Funktionen von createAction und createReducer;
  • createSelector ist eine Funktion aus der Reselect- Bibliothek, die zur Vereinfachung der Verwendung erneut exportiert wird .

Es ist auch erwähnenswert, dass das Redux Toolkit vollständig in TypeScript integriert ist. Weitere Informationen finden Sie im Abschnitt Verwendung mit TypeScript in der offiziellen Dokumentation.


Bewerbung


Ziehen Sie die Verwendung der Redux Toolkit-Bibliothek als Beispiel für ein Fragment einer wirklich verwendeten React Redux-Anwendung in Betracht.
Hinweis Im weiteren Verlauf des Artikels wird der Quellcode sowohl ohne als auch mit dem Redux Toolkit vorgestellt, wodurch die positiven und negativen Aspekte der Verwendung dieser Bibliothek besser bewertet werden können.


Herausforderung


In einer unserer internen Anwendungen mussten Informationen zu Releases unserer Softwareprodukte hinzugefügt, bearbeitet und angezeigt werden. Für jede dieser Aktionen wurden separate API-Funktionen entwickelt, deren Ergebnisse dem Redux-Store hinzugefügt werden müssen. Als Mittel zur Steuerung des asynchronen Verhaltens und der Nebenwirkungen verwenden wir Thunk .


Speichererstellung


Die ursprüngliche Version des Quellcodes, der das Repository erstellt, sah folgendermaßen aus:


import { createStore, applyMiddleware, combineReducers, compose, } from 'redux'; import thunk from 'redux-thunk'; import * as reducers from './reducers'; const ext = window.__REDUX_DEVTOOLS_EXTENSION__; const devtoolMiddleware = ext && process.env.NODE_ENV === 'development' ? ext() : f => f; const store = createStore( combineReducers({ ...reducers, }), compose( applyMiddleware(thunk), devtoolMiddleware ) ); 

Wenn Sie sich den obigen Code sorgfältig ansehen, sehen Sie eine ziemlich lange Abfolge von Aktionen, die ausgeführt werden müssen, damit der Speicher vollständig konfiguriert ist. Das Redux Toolkit enthält ein Tool zur Vereinfachung dieses Vorgangs, nämlich die configureStore-Funktion.


ConfigureStore-Funktion


Mit diesem Tool können Sie Reduzierungen automatisch kombinieren, Redux-Middleware hinzufügen (standardmäßig Redux-Thunk) und auch die Redux DevTools-Erweiterung verwenden. Die configureStore-Funktion akzeptiert ein Objekt mit den folgenden Eigenschaften als Eingabeparameter:


  • Reduzierer - eine Reihe von benutzerdefinierten Reduzierern,
  • Middleware - ein optionaler Parameter, der ein Array von Middleware angibt, das für die Verbindung mit dem Repository entwickelt wurde.
  • devTools - ein logischer Typparameter, mit dem Sie die im Browser installierte Redux DevTools-Erweiterung aktivieren können (der Standardwert ist true),
  • preloadedState - ein optionaler Parameter, der den Anfangszustand des Repositorys festlegt,
  • Enhancer - ein optionaler Parameter, der eine Reihe von Verstärkern definiert.

Um die beliebteste Liste von Middleware zu erhalten, können Sie die Sonderfunktion getDefaultMiddleware verwenden, die ebenfalls Teil des Redux Toolkit ist. Diese Funktion gibt ein Array mit standardmäßig aktivierter Middleware in der Redux Toolkit-Bibliothek zurück. Die Liste dieser Middlewares unterscheidet sich je nach Modus, in dem Ihr Code ausgeführt wird. Im Produktionsmodus besteht ein Array nur aus einem Element - Thunk. Im Entwicklungsmodus wird die Liste zum Zeitpunkt des Schreibens mit der folgenden Middleware ergänzt:


  • serializableStateInvariant - Ein speziell für das Redux Toolkit entwickeltes Tool, mit dem der Statusbaum auf nicht serialisierbare Werte wie Funktionen, Promise, Symbol und andere Werte überprüft werden kann, die keine einfachen JS-Daten sind.
  • immutableStateInvariant - Middleware aus dem Paket redux-immutable-state-invariant , mit der Mutationen in im Repository gespeicherten Daten erkannt werden.

Um eine aufsteigende Liste von Middleware anzugeben, akzeptiert die Funktion getDefaultMidlleware ein Objekt, das die Liste der enthaltenen Middleware und deren Einstellungen definiert. Weitere Informationen zu diesen Informationen finden Sie im entsprechenden Abschnitt der offiziellen Dokumentation.


Jetzt schreiben wir den Code-Abschnitt neu, der für die Erstellung des Repositorys mit den oben beschriebenen Tools verantwortlich ist. Als Ergebnis erhalten wir Folgendes:


 import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import * as reducers from './reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { ...reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', }); 

Anhand des Beispiels dieses Codeabschnitts können Sie deutlich erkennen, dass die configureStore-Funktion die folgenden Probleme löst:


  • die Notwendigkeit, Untersetzungsgetriebe zu kombinieren, wobei combineReducers automatisch aufgerufen wird,
  • Die Notwendigkeit, Middleware zu kombinieren, ruft automatisch applyMiddleware auf.

Außerdem können Sie die Redux DevTools-Erweiterung mit der Funktion composeWithDevTools aus dem Erweiterungspaket redux-devtools bequemer aktivieren. All dies weist darauf hin, dass Sie mit dieser Funktion den Code kompakter und verständlicher gestalten können.


Damit ist die Erstellung und Konfiguration des Repositorys abgeschlossen. Wir übermitteln es an den Anbieter und fahren fort.


Aktionen, Aktionsersteller und Reduzierer


Betrachten wir nun die Funktionen des Redux Toolkit in Bezug auf die Entwicklung von Aktionen, Aktionserstellern und Reduzierern. Die ursprüngliche Version des Codes ohne das Redux Toolkit war in die Dateien actions.js und reducers.js unterteilt. Der Inhalt der Datei actions.js sah folgendermaßen aus:


 import * as productReleasesService from '../../services/productReleases'; export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING'; export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED'; export const PRODUCT_RELEASES_FETCHING_ERROR = 'PRODUCT_RELEASES_FETCHING_ERROR'; … export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING'; export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED'; export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR = 'PRODUCT_RELEASE_CREATING_UPDATING_ERROR'; function productReleasesFetching() { return { type: PRODUCT_RELEASES_FETCHING }; } function productReleasesFetched(productReleases) { return { type: PRODUCT_RELEASES_FETCHED, productReleases }; } function productReleasesFetchingError(error) { return { type: PRODUCT_RELEASES_FETCHING_ERROR, error } } … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched(productReleases)) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError(error)) }); } } … export function updateProductRelease( id, productName, productVersion, releaseDate ) { return dispatch => { dispatch(productReleaseUpdating()); return productReleasesService.updateProductRelease( id, productName, productVersion, releaseDate ).then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } } 

Inhalt der Datei reducers.js vor Verwendung von Redux Toolkit:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', updatingState: 'none', error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case productReleases.PRODUCT_RELEASES_FETCHING: return { ...state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { ...state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { ...state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { ...state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { ...state, updatingState: 'success', productReleases: state.productReleases.map(productRelease => { if (productRelease.id === action.productRelease.id) return action.productRelease; return productRelease; }) }; case productReleases.PRODUCT_RELEASE_UPDATING_ERROR: return { ...state, updatingState: 'failed', error: action.error }; default: return state; } } 

Wie wir sehen können, ist hier der größte Teil des Boilerplates enthalten: Aktionstypkonstanten, Aktionsersteller, wieder Konstanten, aber im Reduzierercode braucht es Zeit, um den gesamten Code zu schreiben. Mit den Funktionen createAction und createReducer, die ebenfalls Teil des Redux Toolkit sind, können Sie dieses Boilerplate teilweise entfernen.


CreateAction-Funktion


Im angegebenen Codeabschnitt wird die Standardmethode zum Definieren einer Aktion in Redux verwendet: Zuerst wird eine Konstante separat definiert, die den Aktionstyp und dann - die Funktion des Erstellers der Aktion dieses Typs bestimmt. Die Funktion createAction kombiniert diese beiden Deklarationen zu einer. Bei der Eingabe wird ein Aktionstyp verwendet und der Ersteller der Aktion für diesen Typ zurückgegeben. Der Aktionsersteller kann entweder ohne Argumente oder mit einem Argument (Nutzlast) aufgerufen werden, dessen Wert in das Nutzlastfeld der erstellten Aktion gestellt wird. Darüber hinaus überschreibt der Aktionsersteller die Funktion toString (), sodass der Aktionstyp zu seiner Zeichenfolgendarstellung wird.


In einigen Fällen müssen Sie möglicherweise zusätzliche Logik schreiben, um den Wert der Nutzlast anzupassen, z. B. mehrere Parameter für den Aktionsersteller akzeptieren, einen zufälligen Bezeichner erstellen oder den aktuellen Zeitstempel abrufen. Zu diesem Zweck verwendet createAction ein optionales zweites Argument - eine Funktion, die zum Aktualisieren des Nutzlastwerts verwendet wird. Weitere Informationen zu diesem Parameter finden Sie in der offiziellen Dokumentation.
Mit der Funktion createAction erhalten wir den folgenden Code:


 export const productReleasesFetching = createAction('PRODUCT_RELEASES_FETCHING'); export const productReleasesFetched = createAction('PRODUCT_RELEASES_FETCHED'); export const productReleasesFetchingError = createAction('PRODUCT_RELEASES_FETCHING_ERROR'); … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } ... 

CreateReducer-Funktion


Betrachten Sie nun das Reduzierstück. Wie in unserem Beispiel werden Reduzierungen häufig mithilfe der switch-Anweisung implementiert, wobei für jeden verarbeiteten Aktionstyp ein Register vorhanden ist. Dieser Ansatz funktioniert gut, ist jedoch nicht ohne Boilerplate und fehleranfällig. Beispielsweise kann leicht vergessen werden, den Standardfall zu beschreiben oder den Anfangszustand nicht festzulegen. Die Funktion createReducer vereinfacht die Erstellung von Reduzierungsfunktionen, indem sie als Funktionssuchtabellen für die Verarbeitung der einzelnen Aktionstypen definiert werden. Außerdem können Sie die Logik unveränderlicher Aktualisierungen erheblich vereinfachen, indem Sie Code in einem "veränderlichen" Stil in Reduzierungen schreiben.


In der Immer- Bibliothek steht ein robuster Stil für die Ereignisbehandlung zur Verfügung. Die Handler-Funktion kann entweder den zum Ändern der Eigenschaften übergebenen Zustand „mutieren“ oder einen neuen Zustand zurückgeben, wie dies bei der Arbeit im unveränderlichen Stil der Fall ist. Dank Immer wird jedoch die eigentliche Mutation des Objekts nicht ausgeführt. Die erste Option erleichtert die Arbeit und die Wahrnehmung erheblich, insbesondere wenn ein Objekt mit tiefer Schachtelung geändert wird.


Seien Sie vorsichtig: Das Zurückgeben eines neuen Objekts von einer Funktion überschreibt „veränderbare“ Änderungen. Die gleichzeitige Verwendung beider Methoden zur Statusaktualisierung funktioniert nicht.


Die Funktion createReducer akzeptiert die folgenden Argumente als Eingabeparameter:


  • Ausgangszustand der Lagerung
  • Ein Objekt, das eine Entsprechung zwischen Aktionstypen und Reduzierern herstellt, von denen jeder einen bestimmten Typ verarbeitet.

Mit der createReducer-Methode erhalten wir den folgenden Code:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const counterReducer = createReducer(initialState, { [productReleasesFetching]: (state, action) => { state.fetchingState = 'requesting' }, [productReleasesFetched.type]: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, [productReleasesFetchingError]: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … [productReleaseUpdating]: (state) => { state.updatingState = 'requesting' }, [productReleaseUpdated]: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, [productReleaseUpdatingError]: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }); 

Wie wir sehen können, löst die Verwendung der Funktionen createAction und createReducer im Wesentlichen das Problem, zusätzlichen Code zu schreiben, aber das Problem, Konstanten vorher zu erstellen, bleibt weiterhin bestehen. Aus diesem Grund betrachten wir eine leistungsfähigere Option, die die Generierung von Aktionserstellern und Reduzierern kombiniert - die Funktion createSlice.


CreateSlice-Funktion


Die Funktion createSlice akzeptiert ein Objekt mit den folgenden Feldern als Eingabeparameter:


  • name - Namespace der erstellten Aktionen ( ${name}/${action.type} );
  • initialState - Anfangszustand des Reduzierers;
  • Reduzierungen - ein Objekt mit Handlern. Jeder Handler übernimmt eine Funktion mit den Argumenten state und action. Action enthält Daten in der Eigenschaft payload und den Namen des Ereignisses in der Eigenschaft name. Darüber hinaus ist es möglich, die vom Ereignis empfangenen Daten vorab zu ändern, bevor sie in den Reduzierer gelangen (z. B. den Elementen der Sammlung eine ID hinzuzufügen). Zu diesem Zweck müssen Sie anstelle einer Funktion ein Objekt mit dem Reduzierer übergeben und Felder vorbereiten, wobei Reduzierer die Action-Handler-Funktion und Prepare die Payload-Handler-Funktion ist, die die aktualisierte Payload zurückgibt.
  • extraReducers - Ein Objekt, das Reduktionen eines anderen Slice enthält. Dieser Parameter kann erforderlich sein, wenn ein Objekt, das zu einem anderen Slice gehört, aktualisiert werden muss. Weitere Informationen zu dieser Funktionalität finden Sie im entsprechenden Abschnitt der offiziellen Dokumentation.

Das Ergebnis der Funktion ist ein Objekt namens "Slice" mit folgenden Feldern:


  • name - scheibenname,
  • Reduzierer - Reduzierer,
  • Aktionen - eine Reihe von Aktionen.

Mit dieser Funktion erhalten wir den folgenden Quellcode:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const productReleases = createSlice({ name: 'productReleases', initialState, reducers: { productReleasesFetching: (state) => { state.fetchingState = 'requesting'; }, productReleasesFetched: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, productReleasesFetchingError: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … productReleaseUpdating: (state) => { state.updatingState = 'requesting' }, productReleaseUpdated: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, productReleaseUpdatingError: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }, }); 

Jetzt extrahieren wir die Aktionsersteller und Reduzierer aus dem erstellten Slice.


 const { actions, reducer } = productReleases; export const { productReleasesFetched, productReleasesFetching, productReleasesFetchingError, … productReleaseUpdated, productReleaseUpdating, productReleaseUpdatingError } = actions; export default reducer; 

Der Quellcode der Aktionsersteller, die die API-Aufrufe enthalten, wurde bis auf die Methode zum Übergeben von Parametern beim Senden von Aktionen nicht geändert:


 export const fetchProductReleases = () => (dispatch) => { dispatch(productReleasesFetching()); return productReleasesService .getProductReleases() .then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .catch((error) => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })); }); }; … export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => { dispatch(productReleaseUpdating()); return productReleasesService .updateProductRelease(id, productName, productVersion, releaseDate) .then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .catch((error) => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseUpdatingError({ error })); }); 

Der obige Code zeigt, dass Sie mit der Funktion createSlice bei der Arbeit mit Redux einen erheblichen Teil der Boilerplate entfernen können, wodurch Sie den Code nicht nur kompakter, prägnanter und verständlicher gestalten, sondern auch weniger Zeit für das Schreiben aufwenden können.


Zusammenfassung


Am Ende dieses Artikels möchte ich darauf hinweisen, dass die Redux Toolkit-Bibliothek zwar nichts Neues zur Speicherverwaltung hinzufügt, jedoch eine Reihe von wesentlich bequemeren Möglichkeiten zum Schreiben von Code bietet als zuvor. Mit diesen Tools kann der Entwicklungsprozess nicht nur bequemer, verständlicher und schneller, sondern auch effektiver gestaltet werden, da eine Reihe bewährter Tools in der Bibliothek vorhanden sind. Wir, Inobitek, planen, diese Bibliothek weiterhin für die Entwicklung unserer Softwareprodukte zu verwenden und neue vielversprechende Entwicklungen im Bereich der Web-Technologien zu überwachen.


Vielen Dank für Ihre Aufmerksamkeit. Wir hoffen, dass unser Artikel nützlich sein wird. Weitere Informationen zur Redux Toolkit-Bibliothek finden Sie in der offiziellen Dokumentation .

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


All Articles