Die Unveränderlichkeit (Unveränderbarkeit/Statizität) in JS und React
Oder warum es in der Zustandsverwaltung 😊 wichtig ist
Hallo zusammen, unser aktueller Artikel befasst sich mit Themen, die einfach erscheinen mögen, aber manchmal können sich selbst erfahrene Entwickler einen Zahn darin brechen (z.B. aufgrund von Schlafmangel der Kinder oder vom Spielen von Baldur's Gate III oder Helldivers II).
Wir geben einen Überblick darüber, was Unveränderlichkeit im Allgemeinen bedeutet, wie sie in der Zustandsverwaltung angezeigt wird und wie wir sie verwenden können.
Am Ende des Artikels werden Sie hoffentlich ein wahrer Meister des Themas sein.
Was ist Unveränderlichkeit?
Unveränderlichkeit ist ein Softwareentwicklungskonzept, das sich darauf bezieht, dass sich bestimmte Daten oder Objekte nach ihrer Erstellung nicht ändern. Mit anderen Worten, Unveränderlichkeit ist eine Eigenschaft, bei der Daten oder Objekte einmal erstellt werden und sich dann nicht ändern.
Wie zeigt sich Unveränderlichkeit in JavaScript, wo kann ich ihr begegnen?
Sofort hat JavaScript unveränderliche Typen (Primitive). JavaScript hat nicht einmal Typen, könnte man sagen, aber wir könnten nicht falscher liegen!
Obwohl es sich um eine dynamisch typisierte Sprache handelt, verfügt JavaScript über Typen, und Variablen müssen nicht angeben, was in sie eingefügt wird (dy-na-mic).
Der typeof-Operator verrät uns den Typ jedes Wertes. Wenn wir im Fall von JavaScript-Primitiven auf eine Eigenschaft zugreifen möchten, erfolgt ein Auto-Boxing, bei dem sie in die entsprechende Wrapper-Klasse eingeschlossen werden, damit ihre charakteristischen Eigenschaften auf sie angewendet werden.
Schauen wir uns das mal an!
var str = "asd"
str.toUpperCase() funktioniert bereits dank Auto-Boxing
str[1] = "h"
console.log(str) // "asd"
Selbst wenn wir versuchen, seinen Wert zu ändern, ist es erfolglos; Die ursprüngliche Zeichenfolge bleibt unverändert.
Wie kann es dann zu Datenmutationen kommen?
Beginnen wir mit meinem Kleinen, der unten als Objekt beschrieben wird.
const baby = {
ageInMonth: 12,
weight: 10,
name: "Alex"
}
Meine Oma nennt das Baby jedoch "Haultsy" (totales Missverständnis, verdient kein Wort). Im Code:
const grandChild = baby
grandChild.name = "Haultsy"
console.log(grandChild)
Als meine Frau nach Hause kommt, stellt sie fest, dass sich der Name des Kindes geändert hat.
console.log(baby.name) // "Haultsy"
Und so beginnt ihre Verwirrung (obwohl sie ansonsten sehr klug und schön ist und alles andere ^^). Alles, was passiert ist, ist, dass in JS das Weitergeben von Werten im Fall von Nicht-Primitiven durch Referenz geschieht und wir unbeabsichtigt den Namen des Babys mutiert haben. Man erkennt ihn nicht mehr 😊.
Auf dieser Grundlage können wir zum nächsten Schritt unseres Artikels übergehen, in dem wir besprechen, was passiert, wenn der React-Zustand mutiert.
Wie können wir Mutationen verhindern?
- Mit den JSON-Methoden parse und stringify
Wie bereits erwähnt, sind JavaScript-Primitive unveränderlich. Eine logische Wahl wäre also, das Objekt zuerst mit JSON.stringify in eine Zeichenfolge zu konvertieren, es dann wieder zu analysieren und das Ergebnis in einer Variablen zu speichern.
In unserem Fall würde das so aussehen:
const grandChild = JSON.parse(JSON.stringify(baby))
grandChild.name = "Haultsy"
console.log(baby.name)
Wir bekommen "Alex", wie es sich gehört.
- Verwenden der Object.assign-Methode
Um zu vermeiden, dass das Objekt als Referenz behandelt wird, erstellen wir in den beiden Fällen zwei und drei ein neues Objekt, speichern es in einer Variablen und ändern es dann.
const grandChild = Object.assign({}, baby)
grandChild.name = "Haultsy"
console.log(baby.name)
Wir bekommen "Alex", wie es sich gehört.
Object.assign Es kopiert den zweiten Parameter in das leere Objekt, das als erster Parameter erstellt wurde, und erzielt so den gewünschten Effekt: ein neues Objekt zu erstellen und nur dessen Eigenschaft "name" zu ändern.
- Verwenden des Spread-Operators
Der unter Punkt 2 beschriebene Effekt kann auch mit dem spread operator erzielt werden.
const grandChild = {...baby}
grandChild.name = "Haultsy"
console.log(baby.name)
Wieder ist das Ergebnis "Alex".
Wir erstellen ein neues Objekt und kopieren den Inhalt des Babyobjekts hinein.
Warnung:
Es gibt ein kleines Problem mit der Art und Weise, wie das Kopieren von Objekten in den Punkten zwei und drei durchgeführt wurde. Obwohl es für unser aktuelles Beispiel perfekt ist, müssen wir uns darüber im Klaren sein, dass dadurch nur eine flache Kopie des angegebenen Objekts erstellt wird. Daher ist es nicht möglich, tief verschachtelte Objekte ordnungsgemäß zu klonen, sodass der Prozess nur bis zu einer Tiefe von einer Ebene erfolgt.
Und hier kommen die nächsten beiden Punkte.
- Strukturiertes Klonen verwenden
Mit der noch relativ neuen globalen Funktion structuredClone() ist es einfach, dieses Problem zu lösen, indem man einen tiefen Klon des gegebenen Objekts erstellt.
Sehen wir es uns in Aktion an:
const grandChild = structuredClone(baby)
grandChild.name = "Haultsy"
console.log(baby.name)
Wieder bekommen wir "Alex".
Jetzt, da die Daten in mehreren Ebenen verschachtelt sind, ist alles in Ordnung. Es gibt keine Referenz mehr zwischen ihnen; Es wurde eine neue Objektinstanz erstellt.
- Verwenden der Unveränderlichkeitsbibliothek
Wenn es möglich ist, die Größe unseres JavaScript-Pakets leicht zu erhöhen, und unsere Teammitglieder bereit sind, zu lernen, wie man es benutzt, könnte die Verwendung einer (Unveränderlichkeitsbibliothek) eine hilfreiche Lösung sein.
Mit seiner Verwendung können solche Probleme dieser Art beseitigt werden.
Unveränderliche Probleme in der React-Zustandsverwaltung: Praxisbeispiel
Ich glaube, dass ein praktisches Beispiel am hilfreichsten ist, anstatt es in einer romanartigen Form zu skizzieren. Lassen Sie uns das Problem mit dem folgenden Code darstellen.
import {useState} from "react"
const initialState = [
{
id: 1,
name: "John",
hobbies: [
{name: "football", id: 60, active: true},
{name: "baseball", id: 61, active: true},
{name: "basketball", id: 62, active: false},
{name: "tennis", id: 63, active: true}
],
friends: [2, 3, 4],
},
{
id: 2,
name: "Jane",
hobbies: [
{name: "knitting", id: 51, active: true},
{name: "sewing", id: 52, active: true},
{name: "reading", id: 53, active: true},
],
friends: [1],
}
]
const Test = () => {
const [data, setData] = useState(initialState)
const handleCeaseFootball = () => {
setData((prev) => prev.map((person) => {
person.hobbies.forEach((hobby) => {
if (hobby.id === 60) {
hobby.active = false
}
})
return person
}))
}
return (
<div className="flex p-40 w-full justify-between">
<pre className="bg-blue-200 p-20">{JSON.stringify(data, null, 1)}</pre>
<div className="flex gap-8 h-full">
<button
className="p-2 bg-amber-100"
onClick={() => setData((prev) => [...prev, {
id: 3,
name: "Andrew",
hobbies: [
{name: "football", id: 60, active: true},
{name: "sword fighting", id: 64, active: true},
{name: "basketball", id: 62, active: false},
{name: "video gaming", id: 65, active: true},
],
friends: [1, 2, 4],
}])}>
Add Andrew friend
</button>
<button
className="p-2 bg-amber-100"
onClick={handleCeaseFootball}>
cease football
</button>
<button
className="p-2 bg-amber-100"
onClick={() => setData(initialState)}>
reset state
</button>
</div>
</div>
)
}
export default Test
Überprüfung des Codes
Wenn wir den Code überprüfen, können wir sehen, dass wir den Zustand der Komponente mit dem Inhalt der initialState-Variablen (einem Array von Objekten) initialisieren.
Es gibt ein Pre-Tag, in dem wir die Daten "anzeigen", zusammen mit drei Schaltflächen.
- Die erste Schaltfläche fügt eine hartcodierte Person zu unserem gespeicherten Zustand hinzu.
- Der zweite Knopf setzt die Fußballaktivität aller Personen auf false.
- Mit der dritten Schaltfläche können wir unseren Zustand auf den Inhalt der initialState-Variablen zurücksetzen und ihn effektiv in seinen ursprünglichen Zustand zurückversetzen.
Nutzen und Schlussfolgerungen ziehen
Erstens können wir im "Display" sehen, dass sich zwei Personen befinden, und nach dem Drücken des ersten Knopfes befinden sich drei Personen im Zustand. Wenn wir genau hinsehen, stellen wir fest, dass zwei Menschen (Andrew und John) Fußball als Hobby haben.
Dann klicken wir auf die zweite Schaltfläche ("Fußball beenden"). Wie erwartet, sehen wir in der "Anzeige", dass sich die Aktivität derjenigen, deren Hobby Fußball ist, in falsch verwandelt hat.
Schließlich versuchen wir, unseren Zustand mit der Schaltfläche "Zustand zurücksetzen" auf die Standardeinstellung zurückzusetzen, aber es geschieht nicht wie erwartet. Der Ausgangszustand wird nicht wiederhergestellt.
Beheben des Problems
Es ist kein Geheimnis, dass das Problem in der handleCeaseFootball-Funktion auftrat, genau dann, wenn die Fußball-Hobby-Aktivität auf false gesetzt wurde (hobby.active = false). Mit dieser Aktion haben wir das Hobby-Array mutiert.
Wie bereits erwähnt, gibt es mehrere Lösungen, von denen wir hier eine mit dem Spread-Operator vorstellen.
const handleCeaseFootball = () => {
setData((prev) => prev.map((person) => ({
...person,
hobbies: person.hobbies.map((hobby) => {
if (hobby.id === 60) {
return {
...hobby,
active: false
}
}
return hobby
})
})))
}
Wir machen eine oberflächliche Kopie sowohl der Menschen als auch ihrer Hobbys. Damit haben wir das korrekte Funktionieren des Codes ohne Datenmutation erreicht.
Fazit
Zusammenfassend lässt sich sagen, dass in dem Artikel das Konzept der Unveränderlichkeit und seine überragende Bedeutung in der JavaScript- und React-Entwicklung ausführlich diskutiert wurden. Anhand von Praxisbeispielen haben wir mögliche Probleme und deren mögliche Lösungen aufgezeigt. Wir haben uns im Detail damit beschäftigt, wie man Fallstricke in der React-Zustandsverwaltung vermeidet und wie man richtig damit umgeht.
Wir hoffen, dass die Informationen in diesem Artikel Ihnen geholfen haben, die Bedeutung der Unveränderlichkeit und ihre praktische Anwendung besser zu verstehen. Wir sind zuversichtlich, dass die hier vorgestellten Beispiele und Tipps Ihnen dabei helfen, React-Zustände effektiv zu verwalten und dadurch die Stabilität und Lesbarkeit des Codes zu verbessern.