Bessere Reduzierer mit Immer
Veröffentlicht: 2022-03-10Als React-Entwickler sollten Sie bereits mit dem Prinzip vertraut sein, dass Zustände nicht direkt mutiert werden sollten. Sie fragen sich vielleicht, was das bedeutet (die meisten von uns hatten diese Verwirrung, als wir anfingen).
Dieses Tutorial wird dem gerecht: Sie werden verstehen, was unveränderlicher Zustand ist und die Notwendigkeit dafür. Sie erfahren auch, wie Sie Immer verwenden, um mit unveränderlichen Zuständen zu arbeiten, und die Vorteile seiner Verwendung. Sie finden den Code in diesem Artikel in diesem Github-Repo.
Unveränderlichkeit in JavaScript und warum es wichtig ist
Immer.js ist eine winzige JavaScript-Bibliothek, die von Michel Weststrate geschrieben wurde, dessen erklärte Mission es ist, Ihnen zu ermöglichen, „auf bequemere Weise mit unveränderlichen Zuständen zu arbeiten“.
Aber bevor wir in Immer eintauchen, lassen Sie uns kurz die Unveränderlichkeit in JavaScript auffrischen und warum sie in einer React-Anwendung wichtig ist.
Der neueste ECMAScript-Standard (alias JavaScript) definiert neun integrierte Datentypen. Von diesen neun Typen gibt es sechs, die als primitive
Werte/Typen bezeichnet werden. Diese sechs Primitive sind undefined
, number
, string
, boolean
Wert , bigint
und symbol
. Eine einfache Prüfung mit dem typeof
-Operator von JavaScript zeigt die Typen dieser Datentypen.
console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint
Ein primitive
ist ein Wert, der kein Objekt ist und keine Methoden hat. Am wichtigsten für unsere gegenwärtige Diskussion ist die Tatsache, dass der Wert eines Grundelements nicht mehr geändert werden kann, sobald es erstellt wurde. Daher werden Primitive als immutable
bezeichnet.
Die verbleibenden drei Typen sind null
, object
und function
. Wir können ihre Typen auch mit dem typeof
Operator überprüfen.
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
Diese Typen sind mutable
. Das bedeutet, dass ihre Werte nach ihrer Erstellung jederzeit geändert werden können.
Sie fragen sich vielleicht, warum ich das Array [0, 1]
dort oben habe. Nun, in JavaScriptland ist ein Array einfach ein spezieller Objekttyp. Falls Sie sich auch über null
wundern und wie es sich von undefined
unterscheidet. undefined
bedeutet einfach, dass wir keinen Wert für eine Variable festgelegt haben, während null
ein Sonderfall für Objekte ist. Wenn Sie wissen, dass etwas ein Objekt sein sollte, aber das Objekt nicht vorhanden ist, geben Sie einfach null
zurück.
Um dies anhand eines einfachen Beispiels zu veranschaulichen, versuchen Sie, den folgenden Code in Ihrer Browserkonsole auszuführen.
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
sollte ein Array zurückgeben, das ein object
ist. Wenn es ein solches Objekt nicht finden kann, gibt es null
zurück. Auch hier würde die Rückgabe von undefined
keinen Sinn machen.
Genug damit. Kehren wir zur Diskussion der Unveränderlichkeit zurück.
Laut den MDN-Dokumenten:
„Alle Typen außer Objekten definieren unveränderliche Werte (d. h. Werte, die nicht geändert werden können).“
Diese Anweisung schließt Funktionen ein, da es sich um einen speziellen Typ von JavaScript-Objekten handelt. Siehe Funktionsdefinition hier.
Werfen wir einen kurzen Blick darauf, was veränderliche und unveränderliche Datentypen in der Praxis bedeuten. Versuchen Sie, den folgenden Code in Ihrer Browserkonsole auszuführen.
let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7
Unsere Ergebnisse zeigen, dass, obwohl b
von a „abgeleitet“ ist, a
Änderung des Werts von b
den Wert von a
nicht beeinflusst. Dies ergibt sich aus der Tatsache, dass die JavaScript-Engine, wenn sie die Anweisung b = a
ausführt, eine neue, separate Speicherstelle erstellt, dort 5
einfügt und b
auf diese Stelle zeigt.
Was ist mit Objekten? Betrachten Sie den folgenden Code.
let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}
Wir können sehen, dass das Ändern der Namenseigenschaft über die Variable d
sie auch in c
ändert. Dies ergibt sich aus der Tatsache, dass, wenn die JavaScript-Engine die Anweisung c = { name: 'some name
'
}
, ausführt, die JavaScript-Engine einen Bereich im Speicher erstellt, das Objekt darin ablegt und c
darauf zeigt. Wenn sie dann die Anweisung d = c
ausführt, zeigt die JavaScript-Engine einfach d
auf dieselbe Stelle. Es wird kein neuer Speicherort erstellt. Daher sind alle Änderungen an den Elementen in d
implizit eine Operation an den Elementen in c
. Ohne viel Aufwand können wir sehen, warum dies Probleme bereitet.
Stellen Sie sich vor, Sie entwickeln eine React-Anwendung und möchten irgendwo den Namen des Benutzers als some name
anzeigen, indem Sie aus der Variablen c
lesen. Aber an anderer Stelle hatten Sie einen Fehler in Ihren Code eingeführt, indem Sie das Objekt d
manipulierten. Dies würde dazu führen, dass der Name des Benutzers als new name
angezeigt wird. Wenn c
und d
Primitive wären, hätten wir dieses Problem nicht. Aber Primitive sind zu einfach für die Arten von Zuständen, die eine typische React-Anwendung aufrechterhalten muss.
Dies ist ungefähr der Hauptgrund, warum es wichtig ist, einen unveränderlichen Zustand in Ihrer Anwendung beizubehalten. Ich ermutige Sie, einige andere Überlegungen zu prüfen, indem Sie diesen kurzen Abschnitt aus der README-Datei von Immutable.js lesen: Argumente für die Unveränderlichkeit.
Nachdem wir verstanden haben, warum wir Unveränderlichkeit in einer React-Anwendung benötigen, werfen wir nun einen Blick darauf, wie Immer das Problem mit seiner produce
-Funktion angeht.
produce
Produktfunktion
Die Kern-API von Immer ist sehr klein, und die Hauptfunktion, mit der Sie arbeiten werden, ist die produce
-Funktion. produce
nimmt einfach einen Anfangszustand und einen Rückruf, der definiert, wie der Zustand geändert werden soll. Der Rückruf selbst erhält eine Entwurfskopie (identisch, aber immer noch eine Kopie) des Zustands, in dem er alle beabsichtigten Aktualisierungen vornimmt. Schließlich wird ein neuer, unveränderlicher Zustand mit allen angewendeten Änderungen produce
.
Das allgemeine Muster für diese Art von Zustandsaktualisierung ist:
// produce signature produce(state, callback) => nextState
Mal sehen, wie das in der Praxis funktioniert.
import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })
Im obigen Code übergeben wir einfach den Startzustand und einen Rückruf, der angibt, wie die Mutationen geschehen sollen. So einfach ist das. Wir müssen keinen anderen Teil des Staates berühren. Es lässt initState
unberührt und teilt strukturell die Teile des Zustands, die wir nicht berührt haben, zwischen den Ausgangs- und den neuen Zuständen. Ein solcher Teil in unserem Staat ist das pets
-Array. Der „ produce
d nextState
ist ein unveränderlicher Zustandsbaum, der sowohl die Änderungen enthält, die wir vorgenommen haben, als auch die Teile, die wir nicht geändert haben.

Bewaffnet mit diesem einfachen, aber nützlichen Wissen, werfen wir einen Blick darauf, wie produce
uns helfen können, unsere React-Reduzierer zu vereinfachen.
Reduzierer mit Immer schreiben
Angenommen, wir haben das unten definierte Zustandsobjekt
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
Und wir wollten ein neues Objekt hinzufügen und in einem nachfolgenden Schritt seinen installed
Schlüssel auf true
setzen
const newPackage = { name: 'immer', installed: false };
Wenn wir dies auf die übliche Weise mit JavaScripts Objekt- und Array-Spread-Syntax tun würden, könnte unser Zustandsreduzierer wie unten aussehen.
const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };
Wir können sehen, dass dies für dieses relativ einfache Zustandsobjekt unnötig ausführlich und fehleranfällig ist. Wir müssen auch jeden Teil des Staates berühren, was unnötig ist. Mal sehen, wie wir dies mit Immer vereinfachen können.
const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
Und mit ein paar Zeilen Code haben wir unseren Reducer stark vereinfacht. Auch wenn wir in den Standardfall fallen, gibt Immer nur den Entwurfsstatus zurück, ohne dass wir etwas tun müssen. Beachten Sie, dass es weniger Boilerplate-Code gibt und die Zustandsverteilung eliminiert wird. Bei Immer kümmern wir uns nur um den Teil des Zustands, den wir aktualisieren möchten. Wenn wir ein solches Element nicht finden können, wie in der Aktion `UPDATE_INSTALLED`, gehen wir einfach weiter, ohne irgendetwas anderes zu berühren. Die „Produce“-Funktion bietet sich auch zum Curryen an. Das Übergeben eines Rückrufs als erstes Argument an „produce“ soll zum Currying verwendet werden. Die Signatur des Curry-„Erzeugnisses“ ist //curried produce signature produce(callback) => (state) => nextState
Mal sehen, wie wir unseren früheren Zustand mit einem Curryprodukt aktualisieren können. Unser Curry-Produkt würde so aussehen: const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
Die Curry-Erzeugnis-Funktion akzeptiert eine Funktion als erstes Argument und gibt ein Curry-Erzeugnis zurück, das erst jetzt einen Zustand benötigt, um den nächsten Zustand zu erzeugen. Das erste Argument der Funktion ist der Entwurfsstatus (der vom Status abgeleitet wird, der beim Aufrufen dieses Curryprodukts übergeben werden soll). Dann folgt jede Anzahl von Argumenten, die wir an die Funktion übergeben möchten.
Alles, was wir jetzt tun müssen, um diese Funktion zu verwenden, ist, den Zustand zu übergeben, aus dem wir den nächsten Zustand und das Aktionsobjekt wie folgt erzeugen möchten.
// add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });
Beachten Sie, dass wir in einer React-Anwendung bei Verwendung des useReducer
den Zustand nicht explizit übergeben müssen, wie ich es oben getan habe, weil es sich darum kümmert.
Du fragst dich vielleicht, ob Immer einen hook
bekommen würde, wie alles in React heutzutage? Nun, Sie sind mit guten Neuigkeiten in Gesellschaft. Immer hat zwei Hooks für die Arbeit mit state: den useImmer
und den useImmerReducer
-Hook. Mal sehen, wie sie funktionieren.
Verwendung der useImmer
und useImmerReducer
Haken
Die beste Beschreibung des useImmer
-Hooks stammt aus der use-immer-README selbst.
useImmer(initialState)
istuseState
sehr ähnlich. Die Funktion gibt ein Tupel zurück, der erste Wert des Tupels ist der aktuelle Zustand, der zweite ist die Updater-Funktion, die eine Immer-Producer-Funktion akzeptiert, in der derdraft
frei mutiert werden kann, bis der Producer endet und die Änderungen vorgenommen werden unveränderlich und werden der nächste Zustand.
Um diese Hooks nutzen zu können, müssen Sie sie zusätzlich zur Immer-Hauptbibliothek separat installieren.
yarn add immer use-immer
In Code-Begriffen sieht der useImmer
Hook wie folgt aus
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
Und so einfach ist das. Man könnte sagen, es ist der useState von React, aber mit ein bisschen Steroid. Die Verwendung der Update-Funktion ist sehr einfach. Es erhält den Entwurfsstatus und Sie können es beliebig ändern, wie unten beschrieben.
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Der Schöpfer von Immer hat ein Codesandbox-Beispiel bereitgestellt, mit dem Sie herumspielen können, um zu sehen, wie es funktioniert.
useImmerReducer
ist ähnlich einfach zu verwenden, wenn Sie den useReducer
-Hook von React verwendet haben. Es hat eine ähnliche Signatur. Mal sehen, wie das in Code-Begriffen aussieht.
import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);
Wir können sehen, dass der Reduzierer einen draft
erhält, den wir beliebig ändern können. Es gibt hier auch ein Codesandbox-Beispiel, mit dem Sie experimentieren können.
Und so einfach ist die Verwendung von Immer-Haken. Aber falls Sie sich immer noch fragen, warum Sie Immer in Ihrem Projekt verwenden sollten, hier ist eine Zusammenfassung einiger der wichtigsten Gründe, die ich für die Verwendung von Immer gefunden habe.
Warum Sie Immer verwenden sollten
Wenn Sie längere Zeit Zustandsverwaltungslogik geschrieben haben, werden Sie die Einfachheit, die Immer bietet, schnell zu schätzen wissen. Aber das ist nicht der einzige Vorteil, den Immer bietet.
Wenn Sie Immer verwenden, schreiben Sie am Ende weniger Boilerplate-Code, wie wir es bei relativ einfachen Reducern gesehen haben. Dies macht auch tiefe Updates relativ einfach.
Bei Bibliotheken wie Immutable.js müssen Sie eine neue API lernen, um die Vorteile der Unveränderlichkeit zu nutzen. Aber mit Immer erreichen Sie dasselbe mit normalen JavaScript Objects
, Arrays
, Sets
und Maps
. Es gibt nichts Neues zu lernen.
Immer bietet standardmäßig auch die strukturelle Freigabe. Das bedeutet einfach, dass Immer automatisch die unveränderten Teile des Zustands zwischen dem neuen Zustand und dem vorherigen Zustand teilt, wenn Sie Änderungen an einem Zustandsobjekt vornehmen.
Mit Immer erhalten Sie auch ein automatisches Einfrieren von Objekten, was bedeutet, dass Sie keine Änderungen am produced
Zustand vornehmen können. Als ich zum Beispiel anfing, Immer zu verwenden, versuchte ich, die Methode sort
auf ein Array von Objekten anzuwenden, die von Immers Produce-Funktion zurückgegeben wurden. Es hat einen Fehler ausgegeben, der mir mitteilte, dass ich keine Änderungen am Array vornehmen kann. Ich musste die Array-Slice-Methode anwenden, bevor ich sort
anwendete. Auch hier ist der erzeugte nextState
ein unveränderlicher Zustandsbaum.
Immer ist auch stark typisiert und mit nur 3 KB sehr klein, wenn es gezippt wird.
Fazit
Wenn es um die Verwaltung von Statusaktualisierungen geht, ist die Verwendung von Immer für mich ein Kinderspiel. Es ist eine sehr leichte Bibliothek, mit der Sie all die Dinge, die Sie über JavaScript gelernt haben, weiterverwenden können, ohne zu versuchen, etwas völlig Neues zu lernen. Ich ermutige Sie, es in Ihrem Projekt zu installieren und sofort zu verwenden. Sie können es in bestehenden Projekten verwenden und Ihre Reduzierer schrittweise aktualisieren.
Ich möchte Sie auch ermutigen, den einführenden Blogbeitrag von Michael Weststrate zu Immer zu lesen. Besonders interessant finde ich den Teil „Wie funktioniert Immer?“. Abschnitt, der erklärt, wie Immer Sprachfunktionen wie Proxys und Konzepte wie Copy-on-Write nutzt.
Ich möchte Sie auch ermutigen, sich diesen Blogbeitrag anzusehen: Immutability in JavaScript: A Contratian View, in dem der Autor, Steven de Salas, seine Gedanken über die Vorzüge des Strebens nach Unveränderlichkeit darlegt.
Ich hoffe, dass Sie mit den Dingen, die Sie in diesem Beitrag gelernt haben, sofort mit der Verwendung von Immer beginnen können.
Ähnliche Resourcen
-
use-immer
, GitHub - Immer, GitHub
-
function
, MDN-Webdokumente, Mozilla -
proxy
, MDN-Webdokumente, Mozilla - Objekt (Informatik), Wikipedia
- „Unveränderlichkeit in JS“, Orji Chidi Matthew, GitHub
- „ECMAScript-Datentypen und -Werte“, Ecma International
- Unveränderliche Sammlungen für JavaScript, Immutable.js , GitHub
- „Der Fall für Unveränderlichkeit“, Immutable.js , GitHub