React, Redux și Immutable.js: Ingrediente pentru aplicații web eficiente
Publicat: 2022-03-11React, Redux și Immutable.js sunt în prezent printre cele mai populare biblioteci JavaScript și devin rapid prima alegere a dezvoltatorilor atunci când vine vorba de dezvoltarea front-end. În cele câteva proiecte React și Redux la care am lucrat, mi-am dat seama că mulți dezvoltatori care au început cu React nu înțeleg pe deplin React și cum să scrie cod eficient pentru a-și folosi întregul potențial.
În acest tutorial Immutable.js, vom construi o aplicație simplă folosind React și Redux și vom identifica unele dintre cele mai frecvente utilizări greșite ale React și modalități de a le evita.
Problemă de referință a datelor
React se referă la performanță. A fost construit de la zero pentru a fi extrem de performant, re-rendând doar părți minime din DOM pentru a satisface noile modificări ale datelor. Orice aplicație React ar trebui să conțină în mare parte componente mici simple (sau funcții fără stat). Sunt simplu de raționat și majoritatea dintre ele pot avea funcția shouldComponentUpdate care returnează false .
shouldComponentUpdate(nextProps, nextState) { return false; }În ceea ce privește performanța, cea mai importantă funcție de ciclu de viață a componentei este shouldComponentUpdate și, dacă este posibil, ar trebui să returneze întotdeauna false . Acest lucru asigură că această componentă nu va reda niciodată (cu excepția randării inițiale), făcând aplicația React să se simtă extrem de rapidă.
Când nu este cazul, scopul nostru este să facem o verificare a egalității ieftine a elementelor de recuzită/stare vechi față de elementele de recuzită/stare noi și să omitem re-rendarea dacă datele sunt neschimbate.
Să facem un pas înapoi pentru o secundă și să examinăm modul în care JavaScript efectuează verificări de egalitate pentru diferite tipuri de date.
Verificarea egalității pentru tipurile de date primitive, cum ar fi boolean , șir și întreg , este foarte simplă, deoarece sunt întotdeauna comparate cu valoarea lor reală:
1 === 1 'string' === 'string' true === truePe de altă parte, verificarea egalității pentru tipuri complexe precum obiecte , matrice și funcții este complet diferită. Două obiecte sunt aceleași dacă au aceeași referință (indicând același obiect în memorie).
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // falseChiar dacă obj1 și obj2 par a fi aceleași, referința lor este diferită. Deoarece sunt diferite, compararea lor naivă în cadrul funcției shouldComponentUpdate va face ca componenta noastră să fie redată inutil.
Lucrul important de remarcat este că datele care provin de la reductoarele Redux, dacă nu sunt configurate corect, vor fi întotdeauna servite cu referințe diferite, ceea ce va face ca componenta să se redea de fiecare dată.
Aceasta este o problemă de bază în încercarea noastră de a evita redarea componentelor.
Manipularea referințelor
Să luăm un exemplu în care avem obiecte profund imbricate și vrem să-l comparăm cu versiunea anterioară. Am putea să parcurgem recursiv elementele de recuzită a obiectelor imbricate și să le comparăm pe fiecare, dar evident că ar fi extrem de costisitor și este exclus.
Asta ne lasă cu o singură soluție, și anume să verificăm referința, dar noi probleme apar rapid:
- Păstrarea referinței dacă nimic nu s-a schimbat
- Schimbarea referinței dacă vreuna dintre valorile obiectului/matricei imbricate a fost modificată
Aceasta nu este o sarcină ușoară dacă vrem să o facem într-un mod frumos, curat și optimizat pentru performanță. Facebook și-a dat seama de această problemă cu mult timp în urmă și a chemat Immutable.js în ajutor.
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 Niciuna dintre funcțiile Immutable.js nu efectuează mutații directe asupra datelor date. În schimb, datele sunt clonate intern, mutate și, dacă au existat modificări, se returnează o nouă referință. În caz contrar, returnează referința inițială. Referința nouă trebuie setată în mod explicit, cum ar fi obj1 = obj1.set(...); .
Exemple React, Redux și Immutable.js
Cel mai bun mod de a demonstra puterea acestor biblioteci este de a construi o aplicație simplă. Și ce poate fi mai simplu decât o aplicație todo?
Pentru concizie, în acest articol, vom parcurge doar părțile aplicației care sunt esențiale pentru aceste concepte. Întregul cod sursă al codului aplicației poate fi găsit pe GitHub.
Când aplicația este pornită, veți observa că apelurile către console.log sunt plasate convenabil în zone cheie pentru a arăta în mod clar cantitatea de redare DOM, care este minimă.
Ca orice altă aplicație de tot, vrem să arătăm o listă de articole de tot. Când utilizatorul face clic pe un articol de făcut, îl vom marca ca finalizat. De asemenea, avem nevoie de un mic câmp de intrare în partea de sus pentru a adăuga noi toate și în partea de jos a 3 filtre care vor permite utilizatorului să comute între:
- Toate
- Efectuat
- Activ
Redux Reducer
Toate datele din aplicația Redux trăiesc în interiorul unui singur obiect magazin și putem privi reductoarele doar ca pe o modalitate convenabilă de a împărți magazinul în bucăți mai mici, care sunt mai ușor de raționat. Deoarece reductorul este, de asemenea, o funcție, și acesta poate fi împărțit în părți și mai mici.
Reductorul nostru va fi format din 2 piese mici:
- todoList
- ActiveFilter
// 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, });Conectarea cu Redux
Acum că am configurat un reductor Redux cu date Immutable.js, să-l conectăm cu componenta React pentru a transmite datele.
// 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);Într-o lume perfectă, conectarea ar trebui să fie efectuată numai pe componente de traseu de nivel superior, extragerea datelor din mapStateToProps, iar restul este de bază React care transmite recuzită copiilor. În aplicațiile la scară largă, tinde să devină greu de urmărit toate conexiunile, așa că vrem să le menținem la minimum.
Este foarte important de reținut că state.todos este un obiect JavaScript obișnuit returnat de la funcția Redux combineReducers (todos fiind numele reductorului), dar state.todos.todoList este o listă imuabilă și este esențial ca acesta să rămână într-o astfel de listă. formularul până când trece verificarea shouldComponentUpdate .
Evitarea redării componentelor
Înainte de a săpă mai adânc, este important să înțelegem ce tip de date trebuie furnizate componentei:
- Tipuri primitive de orice fel
- Obiect/matrice numai în formă imuabilă
Având aceste tipuri de date, ne permite să comparăm superficial elementele de recuzită care vin în componentele React.
Următorul exemplu arată cum să diferențieți elementele de recuzită în cel mai simplu mod posibil:
$ npm install react-pure-render import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }Funcția shallowEqual va verifica diferența dintre props/starea doar la 1 nivel adânc. Funcționează extrem de rapid și este în sinergie perfectă cu datele noastre imuabile. A trebui să scrieți acest shouldComponentUpdate în fiecare componentă ar fi foarte incomod, dar, din fericire, există o soluție simplă.

Extrageți shouldComponentUpdate într-o componentă specială separată:
// 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); } }Apoi extindeți orice componentă în care se dorește această logică shouldComponentUpdate:
// components/Todo.js export default class Todo extends PureComponent { // Component code }Aceasta este o modalitate foarte curată și eficientă de a evita redarea componentelor în majoritatea cazurilor, iar ulterior, dacă aplicația devine mai complexă și brusc necesită o soluție personalizată, poate fi schimbată cu ușurință.
Există o ușoară problemă când utilizați PureComponent în timp ce treceți funcții ca elemente de recuzită. Deoarece React, cu clasa ES6, nu leagă automat acest lucru la funcțiile, trebuie să o facem manual. Putem realiza acest lucru făcând una dintre următoarele:
- utilizați legarea funcției săgeată ES6:
<Component onClick={() => this.handleClick()} /> - utilizați bind :
<Component onClick={this.handleClick.bind(this)} />
Ambele abordări vor face ca Componenta să se redea, deoarece referință diferită a fost transmisă la onClick de fiecare dată.
Pentru a rezolva această problemă, putem pre-lega funcții în metoda constructorului astfel:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }Dacă de cele mai multe ori pre-legați mai multe funcții, putem exporta și reutiliza funcția de ajutor mică:
// 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 }Dacă niciuna dintre soluții nu funcționează pentru dvs., puteți oricând să scrieți manual condițiile shouldComponentUpdate .
Gestionarea datelor imuabile în interiorul unei componente
Odată cu configurarea actuală a datelor imuabile, re-rendarea a fost evitată și am rămas cu date imuabile în interiorul elementelor de recuzită ale unei componente. Există o serie de moduri de a utiliza aceste date imuabile, dar cea mai comună greșeală este de a converti datele imediat în JS simplu folosind funcția imuabilă în JS.
Utilizarea toJS pentru a converti profund datele imuabile în JS simplu anulează întregul scop de a evita redarea din nou, deoarece, așa cum era de așteptat, este foarte lent și, ca atare, ar trebui evitat. Deci, cum gestionăm datele imuabile?
Trebuie utilizat ca atare, de aceea Immutable API oferă o mare varietate de funcții, mapează și obțin fiind cel mai frecvent utilizate în interiorul componentei React. Structura de date todoList care provine de la Redux Reducer este o matrice de obiecte în formă imuabilă, fiecare obiect reprezentând un singur element todo:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]API-ul Immutable.js este foarte asemănător cu JavaScript obișnuit, așa că am folosi todoList ca orice altă matrice de obiecte. Funcția de hartă se dovedește cea mai bună în majoritatea cazurilor.
În interiorul unui callback de hartă obținem todo , care este un obiect încă în formă imuabilă și îl putem trece în siguranță în componenta Todo .
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }Dacă intenționați să efectuați mai multe iterații înlănțuite peste date imuabile, cum ar fi:
myMap.filter(somePred).sort(someComp)… atunci este foarte important să îl convertiți mai întâi în Seq folosind toSeq și, după iterații, să îl întoarceți la forma dorită, cum ar fi:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()Deoarece Immutable.js nu modifică niciodată în mod direct datele date, trebuie întotdeauna să facă o altă copie a acestora, efectuarea mai multor iterații ca aceasta poate fi foarte costisitoare. Seq este o secvență imuabilă leneșă de date, ceea ce înseamnă că va efectua cât mai puține operațiuni posibil pentru a-și îndeplini sarcina, în timp ce omite crearea de copii intermediare. Seq a fost construit pentru a fi folosit în acest fel.
În interiorul componentei Todo , utilizați get sau getIn pentru a obține recuzita.
Destul de simplu, nu?
Ei bine, ceea ce mi-am dat seama este că de multe ori poate deveni foarte ilizibil având un număr mare de get() și mai ales getIn() . Așa că am decis să găsesc un punct favorabil între performanță și lizibilitate și după câteva experimente simple am aflat că funcțiile toObject și toArray Immutable.js funcționează foarte bine.
Aceste funcții convertesc puțin (în adâncime la un nivel) obiectele/matricele Immutable.js în obiecte/matrice JavaScript simple. Dacă avem date profund imbricate în interior, acestea vor rămâne în formă imuabilă gata pentru a fi transmise
Este mai lent decât get() doar cu o marjă neglijabilă, dar arată mult mai curat:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }Să vedem totul în acțiune
În cazul în care nu ați clonat încă codul din GitHub, acum este momentul potrivit să o faceți:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutablePornirea serverului este la fel de simplă (asigurați-vă că Node.js și NPM sunt instalate) ca acesta:
npm install npm start Navigați la http://localhost:3000 în browserul dvs. web. Cu consola pentru dezvoltatori deschisă, urmăriți jurnalele în timp ce adăugați câteva elemente de rezolvat, marcați-le ca finalizate și schimbați filtrul:
- Adăugați 5 articole de tot
- Schimbați filtrul de la „Toate” la „Activ” și apoi înapoi la „Toate”
- Fără redare, doar schimbarea filtrului
- Marcați 2 articole ca finalizate
- Două toate au fost redate din nou, dar numai câte unul
- Schimbați filtrul de la „Toate” la „Activ” și apoi înapoi la „Toate”
- Au fost montate/demontate doar 2 articole finalizate
- Cele active nu au fost redate din nou
- Ștergeți un singur articol de făcut din mijlocul listei
- Numai elementul de toaletă eliminat a fost afectat, altele nu au fost redate din nou
Învelire
Sinergia dintre React, Redux și Immutable.js, atunci când este utilizat corect, oferă câteva soluții elegante la multe probleme de performanță care sunt adesea întâlnite în aplicațiile web mari.
Immutable.js ne permite să detectăm modificări în obiectele/matricele JavaScript fără a recurge la ineficiența verificărilor profunde de egalitate, ceea ce, la rândul său, permite React să evite operațiunile costisitoare de redare atunci când acestea nu sunt necesare. Aceasta înseamnă că performanța Immutable.js tinde să fie bună în majoritatea scenariilor.
Sper că ți-a plăcut articolul și îl vei găsi util pentru a construi soluții inovatoare React în proiectele tale viitoare.
