React, Redux und Immutable.js: Zutaten für effiziente Webanwendungen
Veröffentlicht: 2022-03-11React, Redux und Immutable.js gehören derzeit zu den beliebtesten JavaScript-Bibliotheken und werden schnell zur ersten Wahl von Entwicklern, wenn es um die Frontend-Entwicklung geht. In den wenigen React- und Redux-Projekten, an denen ich gearbeitet habe, habe ich festgestellt, dass viele Entwickler, die mit React anfangen, React nicht vollständig verstehen und wissen, wie man effizienten Code schreibt, um sein volles Potenzial auszuschöpfen.
In diesem Immutable.js-Tutorial werden wir eine einfache App mit React und Redux erstellen und einige der häufigsten Fehlanwendungen von React und Möglichkeiten zu ihrer Vermeidung identifizieren.
Datenreferenzproblem
Bei React dreht sich alles um Leistung. Es wurde von Grund auf entwickelt, um extrem leistungsfähig zu sein und nur minimale Teile von DOM neu zu rendern, um neuen Datenänderungen gerecht zu werden. Jede React-App sollte hauptsächlich aus kleinen einfachen (oder zustandslosen Funktions-) Komponenten bestehen. Sie sind einfach zu begründen und die meisten von ihnen können die Funktion shouldComponentUpdate haben, die false zurückgibt.
shouldComponentUpdate(nextProps, nextState) { return false; }
In Bezug auf die Leistung ist die wichtigste Lebenszyklusfunktion der Komponente shouldComponentUpdate und sollte nach Möglichkeit immer false zurückgeben. Dadurch wird sichergestellt, dass diese Komponente niemals erneut gerendert wird (außer dem anfänglichen Rendering), wodurch sich die React-App effektiv extrem schnell anfühlt.
Wenn dies nicht der Fall ist, ist es unser Ziel, eine billige Gleichheitsprüfung von alten Requisiten/Status gegenüber neuen Requisiten/Status durchzuführen und das erneute Rendern zu überspringen, wenn die Daten unverändert sind.
Lassen Sie uns für eine Sekunde einen Schritt zurücktreten und uns ansehen, wie JavaScript Gleichheitsprüfungen für verschiedene Datentypen durchführt.
Die Gleichheitsprüfung für primitive Datentypen wie boolean , string und integer ist sehr einfach, da sie immer mit ihrem tatsächlichen Wert verglichen werden:
1 === 1 'string' === 'string' true === true
Auf der anderen Seite ist die Gleichheitsprüfung für komplexe Typen wie Objekte , Arrays und Funktionen völlig anders. Zwei Objekte sind gleich, wenn sie die gleiche Referenz haben (auf das gleiche Objekt im Speicher zeigen).
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false
Obwohl obj1 und obj2 gleich zu sein scheinen, ist ihre Referenz unterschiedlich. Da sie unterschiedlich sind, führt ein naiver Vergleich innerhalb der shouldComponentUpdate -Funktion dazu, dass unsere Komponente unnötig neu gerendert wird.
Es ist wichtig zu beachten, dass die Daten, die von Redux-Reduzierern kommen, wenn sie nicht korrekt eingerichtet sind, immer mit einer anderen Referenz geliefert werden, was dazu führt, dass die Komponente jedes Mal neu gerendert wird.
Dies ist ein Kernproblem in unserem Bestreben, das erneute Rendern von Komponenten zu vermeiden.
Umgang mit Referenzen
Nehmen wir ein Beispiel, in dem wir tief verschachtelte Objekte haben und wir es mit seiner vorherigen Version vergleichen möchten. Wir könnten verschachtelte Objektstützen rekursiv durchlaufen und jede vergleichen, aber das wäre natürlich extrem teuer und kommt nicht in Frage.
Damit bleibt uns nur eine Lösung, nämlich die Referenz zu überprüfen, aber schnell tauchen neue Probleme auf:
- Beibehalten der Referenz, wenn sich nichts geändert hat
- Ändern der Referenz, wenn sich einer der verschachtelten Objekt-/Array-Prop-Werte geändert hat
Das ist keine leichte Aufgabe, wenn wir es schön, sauber und leistungsoptimiert machen wollen. Facebook hat dieses Problem schon vor langer Zeit erkannt und Immutable.js zu Hilfe gerufen.
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference
Keine der Immutable.js-Funktionen führt eine direkte Mutation an den angegebenen Daten durch. Stattdessen werden die Daten intern geklont, mutiert und bei Änderungen wird eine neue Referenz zurückgegeben. Andernfalls wird die anfängliche Referenz zurückgegeben. Neue Referenz muss explizit gesetzt werden, wie obj1 = obj1.set(...);
.
Beispiele für React, Redux und Immutable.js
Der beste Weg, die Leistungsfähigkeit dieser Bibliotheken zu demonstrieren, besteht darin, eine einfache App zu erstellen. Und was kann einfacher sein als eine Todo-App?
Der Kürze halber werden wir in diesem Artikel nur die Teile der App durchgehen, die für diese Konzepte entscheidend sind. Der gesamte Quellcode des App-Codes ist auf GitHub zu finden.
Wenn die App gestartet wird, werden Sie feststellen, dass Aufrufe von console.log bequem in Schlüsselbereichen platziert werden, um die Menge an DOM-Neurendering, die minimal ist, deutlich zu zeigen.
Wie jede andere Todo-App möchten wir eine Liste mit Todo-Elementen anzeigen. Wenn der Benutzer auf ein Aufgabenelement klickt, markieren wir es als erledigt. Außerdem brauchen wir oben ein kleines Eingabefeld, um neue Todos hinzuzufügen, und unten 3 Filter, mit denen der Benutzer umschalten kann zwischen:
- Alle
- Abgeschlossen
- Aktiv
Redux-Reduzierer
Alle Daten in der Redux-Anwendung befinden sich in einem einzigen Speicherobjekt, und wir können die Reduzierungen als eine bequeme Möglichkeit betrachten, den Speicher in kleinere Teile aufzuteilen, über die leichter nachgedacht werden kann. Da Reducer auch eine Funktion ist, kann sie auch in noch kleinere Teile aufgeteilt werden.
Unser Reduzierstück besteht aus 2 kleinen Teilen:
- Aufgabenliste
- aktivFilter
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });
Verbindung mit Redux
Nachdem wir nun einen Redux Reducer mit Immutable.js-Daten eingerichtet haben, verbinden wir ihn mit der React-Komponente, um die Daten zu übergeben.
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);
In einer perfekten Welt sollte Connect nur für Routenkomponenten der obersten Ebene ausgeführt werden, wobei die Daten in mapStateToProps extrahiert werden und der Rest einfach React ist, indem Requisiten an Kinder weitergegeben werden. Bei umfangreichen Anwendungen wird es oft schwierig, alle Verbindungen im Auge zu behalten, daher möchten wir sie auf ein Minimum beschränken.
Es ist sehr wichtig zu beachten, dass state.todos ein reguläres JavaScript-Objekt ist, das von der Redux-Funktion CombineReducers zurückgegeben wird (todos ist der Name des Reducers), aber state.todos.todoList ist eine unveränderliche Liste, und es ist wichtig, dass sie in einer solchen bleibt Formular, bis es die shouldComponentUpdate- Prüfung besteht.
Vermeiden des erneuten Renderns von Komponenten
Bevor wir tiefer graben, ist es wichtig zu verstehen, welche Art von Daten der Komponente bereitgestellt werden müssen:
- Primitive Arten jeglicher Art
- Objekt/Array nur in unveränderlicher Form
Mit diesen Datentypen können wir die Requisiten, die in React-Komponenten enthalten sind, oberflächlich vergleichen.
Das nächste Beispiel zeigt, wie man die Requisiten auf einfachste Weise unterscheidet:
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }
Die Funktion shallowEqual prüft den Props/State Diff nur 1 Level tief. Es arbeitet extrem schnell und ist in perfekter Synergie mit unseren unveränderlichen Daten. Dieses shouldComponentUpdate in jede Komponente schreiben zu müssen, wäre sehr umständlich, aber zum Glück gibt es eine einfache Lösung.

Extrahieren Sie shouldComponentUpdate in eine spezielle separate Komponente:
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }
Erweitern Sie dann einfach eine beliebige Komponente, in der diese shouldComponentUpdate-Logik erwünscht ist:
// components/Todo.js export default class Todo extends PureComponent { // Component code }
Dies ist in den meisten Fällen eine sehr saubere und effiziente Methode, um das erneute Rendern von Komponenten zu vermeiden, und wenn die App später komplexer wird und plötzlich eine benutzerdefinierte Lösung erfordert, kann sie einfach geändert werden.
Es gibt ein kleines Problem bei der Verwendung von PureComponent , während Funktionen als Requisiten übergeben werden. Da React mit ES6 class dies nicht automatisch an Funktionen bindet, müssen wir es manuell tun. Wir können dies erreichen, indem wir einen der folgenden Schritte ausführen:
- Verwenden Sie die ES6-Pfeilfunktionsbindung:
<Component onClick={() => this.handleClick()} />
- use bind :
<Component onClick={this.handleClick.bind(this)} />
Beide Ansätze führen dazu, dass Component neu gerendert wird, da jedes Mal eine andere Referenz an onClick übergeben wurde.
Um dieses Problem zu umgehen, können wir Funktionen in der Konstruktormethode wie folgt vorbinden:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }
Wenn Sie die meiste Zeit mehrere Funktionen vorab binden, können wir kleine Hilfsfunktionen exportieren und wiederverwenden:
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }
Wenn keine der Lösungen für Sie funktioniert, können Sie shouldComponentUpdate- Bedingungen immer manuell schreiben.
Umgang mit unveränderlichen Daten innerhalb einer Komponente
Mit der aktuellen Einrichtung unveränderlicher Daten wurde ein erneutes Rendern vermieden, und wir haben unveränderliche Daten in den Requisiten einer Komponente. Es gibt eine Reihe von Möglichkeiten, diese unveränderlichen Daten zu verwenden, aber der häufigste Fehler besteht darin, die Daten mithilfe der unveränderlichen zu JS- Funktion sofort in einfaches JS zu konvertieren.
Die Verwendung von toJS , um unveränderliche Daten tief in einfaches JS zu konvertieren, verneint den gesamten Zweck, ein erneutes Rendern zu vermeiden, da dies erwartungsgemäß sehr langsam ist und daher vermieden werden sollte. Wie gehen wir also mit unveränderlichen Daten um?
Es muss so verwendet werden, wie es ist, deshalb bietet die Immutable API eine Vielzahl von Funktionen, Karten und wird am häufigsten innerhalb der React-Komponente verwendet. Die todoList -Datenstruktur, die vom Redux Reducer stammt, ist ein Array von Objekten in unveränderlicher Form, wobei jedes Objekt ein einzelnes Todo-Element darstellt:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]
Die Immutable.js-API ist dem regulären JavaScript sehr ähnlich, daher würden wir todoList wie jedes andere Array von Objekten verwenden. Die Kartenfunktion erweist sich in den meisten Fällen als am besten.
Innerhalb eines Map-Callbacks erhalten wir todo , ein Objekt, das sich immer noch in unveränderlicher Form befindet, und wir können es sicher in der Todo -Komponente übergeben.
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }
Wenn Sie vorhaben, mehrere verkettete Iterationen über unveränderliche Daten durchzuführen, wie z.
myMap.filter(somePred).sort(someComp)
… dann ist es sehr wichtig, es zuerst mit toSeq in Seq umzuwandeln und es nach Iterationen wieder in die gewünschte Form zu bringen, wie:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
Da Immutable.js gegebene Daten niemals direkt mutiert, muss immer eine weitere Kopie davon erstellt werden, was mehrere Iterationen wie diese sehr teuer machen kann. Seq ist eine faule unveränderliche Datensequenz, was bedeutet, dass sie so wenige Operationen wie möglich ausführt, um ihre Aufgabe zu erledigen, während die Erstellung von Zwischenkopien übersprungen wird. Seq wurde entwickelt, um auf diese Weise verwendet zu werden.
Verwenden Sie innerhalb der Todo -Komponente get oder getIn , um die Requisiten abzurufen.
Einfach genug, oder?
Nun, was mir klar wurde, ist, dass es oft sehr unlesbar werden kann, wenn eine große Anzahl von get()
und insbesondere getIn()
. Also beschloss ich, einen Sweetspot zwischen Leistung und Lesbarkeit zu finden, und nach einigen einfachen Experimenten fand ich heraus, dass die toObject- und toArray- Funktionen von Immutable.j sehr gut funktionieren.
Diese Funktionen konvertieren oberflächlich (1 Ebene tief) Immutable.js-Objekte/Arrays in einfache JavaScript-Objekte/Arrays. Wenn wir Daten tief im Inneren verschachtelt haben, bleiben sie in unveränderlicher Form bereit, an die sie weitergegeben werden können
Es ist nur geringfügig langsamer als get()
, sieht aber viel sauberer aus:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }
Sehen wir uns alles in Aktion an
Falls Sie den Code von GitHub noch nicht geklont haben, ist jetzt ein guter Zeitpunkt, dies zu tun:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable
Das Starten des Servers ist so einfach (stellen Sie sicher, dass Node.js und NPM installiert sind) wie folgt:
npm install npm start
Navigieren Sie in Ihrem Webbrowser zu http://localhost:3000. Sehen Sie sich bei geöffneter Entwicklerkonsole die Protokolle an, während Sie einige Aufgaben hinzufügen, markieren Sie sie als erledigt und ändern Sie den Filter:
- Fügen Sie 5 Aufgaben hinzu
- Ändern Sie den Filter von „Alle“ auf „Aktiv“ und dann zurück auf „Alle“.
- Kein erneutes Rendern erforderlich, nur Filterwechsel
- Markieren Sie 2 Aufgaben als erledigt
- Zwei Todos wurden neu gerendert, aber jeweils nur einer
- Ändern Sie den Filter von „Alle“ auf „Aktiv“ und dann zurück auf „Alle“.
- Nur 2 abgeschlossene Todo-Elemente wurden montiert/demontiert
- Aktive wurden nicht neu gerendert
- Löschen Sie ein einzelnes Aufgabenelement aus der Mitte der Liste
- Nur das entfernte Aufgabenelement war betroffen, andere wurden nicht erneut gerendert
Einpacken
Die Synergie von React, Redux und Immutable.js bietet bei richtiger Anwendung einige elegante Lösungen für viele Leistungsprobleme, die häufig in großen Webanwendungen auftreten.
Immutable.js ermöglicht es uns, Änderungen in JavaScript-Objekten/Arrays zu erkennen, ohne auf die Ineffizienz von Deep-Equality-Checks zurückzugreifen, was es React wiederum ermöglicht, teure Re-Rendering-Operationen zu vermeiden, wenn sie nicht erforderlich sind. Dies bedeutet, dass die Leistung von Immutable.js in den meisten Szenarien gut ist.
Ich hoffe, Ihnen hat der Artikel gefallen und Sie finden ihn nützlich, um innovative React-Lösungen für Ihre zukünftigen Projekte zu entwickeln.