Reductoare mai bune cu Immer

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, vom învăța cum să folosim Immer pentru a scrie reductoare. Când lucrăm cu React, menținem multă stare. Pentru a face actualizări ale stării noastre, trebuie să scriem o mulțime de reductoare. Scrierea manuală a reductoarelor are ca rezultat codul umflat în care trebuie să atingem aproape fiecare parte a stării noastre. Acest lucru este plictisitor și predispus la erori. În acest articol, vom vedea cum Immer aduce mai multă simplitate procesului de scriere a reductoarelor de stare.

În calitate de dezvoltator React, ar trebui să fiți deja familiarizați cu principiul conform căruia starea nu trebuie mutată direct. S-ar putea să vă întrebați ce înseamnă asta (cei mai mulți dintre noi au avut acea confuzie când am început).

Acest tutorial va face dreptate: veți înțelege ce este starea imuabilă și necesitatea acesteia. De asemenea, veți învăța cum să utilizați Immer pentru a lucra cu stare imuabilă și beneficiile utilizării acestuia. Puteți găsi codul în acest articol în acest depozit Github.

Imuabilitatea în JavaScript și de ce contează

Immer.js este o mică bibliotecă JavaScript a fost scrisă de Michel Weststrate a cărei misiune declarată este să vă permită „să lucrați cu o stare imuabilă într-un mod mai convenabil”.

Dar înainte de a ne scufunda în Immer, haideți să facem rapid o actualizare despre imuabilitatea în JavaScript și de ce este importantă într-o aplicație React.

Cel mai recent standard ECMAScript (alias JavaScript) definește nouă tipuri de date încorporate. Dintre aceste nouă tipuri, există șase care sunt denumite valori/tipuri primitive . Aceste șase primitive sunt undefined , number , string , boolean , bigint și symbol . O simplă verificare cu typeof operatorului JavaScript va dezvălui tipurile acestor tipuri de date.

 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

O primitive este o valoare care nu este un obiect și nu are metode. Cel mai important pentru discuția noastră actuală este faptul că valoarea unui primitiv nu poate fi schimbată odată ce este creat. Astfel, se spune că primitivele sunt immutable .

Celelalte trei tipuri sunt null , object și function . De asemenea, putem verifica tipurile lor folosind operatorul typeof .

 console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function

Aceste tipuri sunt mutable . Aceasta înseamnă că valorile lor pot fi modificate în orice moment după ce au fost create.

S-ar putea să vă întrebați de ce am matricea [0, 1] acolo sus. Ei bine, în JavaScriptland, o matrice este pur și simplu un tip special de obiect. În cazul în care vă întrebați și despre null și cum este diferit de undefined . undefined înseamnă pur și simplu că nu am setat o valoare pentru o variabilă, în timp ce null este un caz special pentru obiecte. Dacă știți că ceva ar trebui să fie un obiect, dar obiectul nu este acolo, pur și simplu returnați null .

Pentru a ilustra cu un exemplu simplu, încercați să rulați codul de mai jos în consola browserului dvs.

 console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]

String.prototype.match ar trebui să returneze o matrice, care este un tip de object . Când nu poate găsi un astfel de obiect, returnează null . Revenirea undefined nu ar avea sens nici aici.

Ajunge cu asta. Să revenim la discutarea imuabilității.

Mai multe după săritură! Continuați să citiți mai jos ↓

Conform documentelor MDN:

„Toate tipurile, cu excepția obiectelor, definesc valori imuabile (adică, valori care nu pot fi modificate).”

Această declarație include funcții deoarece sunt un tip special de obiect JavaScript. Vedeți definiția funcției aici.

Să aruncăm o privire rapidă la ce înseamnă în practică tipurile de date mutabile și imuabile. Încercați să rulați codul de mai jos în consola browserului dvs.

 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

Rezultatele noastre arată că, deși b este „derivat” din a , modificarea valorii lui b nu afectează valoarea lui a . Acest lucru rezultă din faptul că atunci când motorul JavaScript execută instrucțiunea b = a , creează o locație de memorie nouă, separată, pune 5 acolo și punctează b în acea locație.

Dar obiectele? Luați în considerare codul de mai jos.

 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"}

Putem vedea că schimbarea proprietății nume prin variabila d o schimbă și în c . Acest lucru rezultă din faptul că atunci când motorul JavaScript execută instrucțiunea, c = { name: 'some name ' } , motorul JavaScript creează un spațiu în memorie, pune obiectul înăuntru și indică c spre el. Apoi, când execută instrucțiunea d = c , motorul JavaScript indică doar d către aceeași locație. Nu creează o nouă locație de memorie. Astfel, orice modificare a elementelor din d este implicit o operație asupra elementelor din c . Fără mult efort, putem vedea de ce aceasta este o problemă în devenire.

Imaginați-vă că dezvoltați o aplicație React și undeva doriți să afișați numele utilizatorului ca some name citind din variabila c . Dar în altă parte ați introdus un bug în codul dvs. prin manipularea obiectului d . Astfel, numele utilizatorului va apărea ca new name . Dacă c și d ar fi primitive nu am avea această problemă. Dar primitivele sunt prea simple pentru tipurile de stare pe care trebuie să le mențină o aplicație tipică React.

Acesta este despre motivele majore pentru care este important să mențineți o stare imuabilă în aplicația dvs. Vă încurajez să verificați alte câteva considerente citind această scurtă secțiune din Immutable.js README: the case for imuability.

După ce am înțeles de ce avem nevoie de imuabilitate într-o aplicație React, haideți acum să aruncăm o privire la modul în care Immer abordează problema cu funcția de produce .

Funcția de produce a lui Immer

API-ul de bază al lui Immer este foarte mic, iar funcția principală cu care veți lucra este funcția produce . produce pur și simplu ia o stare inițială și un callback care definește modul în care starea ar trebui să fie mutată. Callback-ul în sine primește o copie nefinalizată (identică, dar totuși o copie) a stării în care face toate actualizările intenționate. În cele din urmă, produce o stare nouă, imuabilă, cu toate modificările aplicate.

Modelul general pentru acest tip de actualizare de stare este:

 // produce signature produce(state, callback) => nextState

Să vedem cum funcționează acest lucru în practică.

 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) })

În codul de mai sus, pur și simplu trecem starea de pornire și un apel invers care specifică cum vrem să se întâmple mutațiile. E la fel de simplu. Nu trebuie să atingem nicio altă parte a statului. Lasă initState neatins și împărtășește structural acele părți ale statului pe care nu le-am atins între statele inițiale și cele noi. O astfel de parte din statul nostru este matricea de pets de companie. produce d nextState este un arbore de stări imuabil care are modificările pe care le-am făcut, precum și părțile pe care nu le-am modificat.

Înarmați cu aceste cunoștințe simple, dar utile, să aruncăm o privire la modul în care produce ne pot ajuta să simplificăm reductoarele React.

Scriere reductoare cu immer

Să presupunem că avem obiectul de stare definit mai jos

 const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };

Și am vrut să adăugăm un nou obiect și, la un pas ulterior, să setăm cheia installed la true

 const newPackage = { name: 'immer', installed: false };

Dacă ar fi să facem acest lucru în modul obișnuit cu sintaxa de răspândire a obiectelor și a matricei JavaScript, reductorul nostru de stare ar putea arăta ca mai jos.

 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; } };

Putem vedea că acest lucru este inutil de verbos și predispus la greșeli pentru acest obiect de stare relativ simplu. De asemenea, trebuie să atingem fiecare parte a statului, ceea ce este inutil. Să vedem cum putem simplifica acest lucru cu Immer.

 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; } });
Și cu câteva linii de cod, ne-am simplificat foarte mult reductorul. De asemenea, dacă intrăm în cazul implicit, Immer doar returnează starea de proiect fără ca noi să fim nevoiți să facem nimic. Observați cum există mai puțin cod boilerplate și eliminarea răspândirii de stat. Cu Immer, ne preocupăm doar de partea statului pe care vrem să o actualizăm. Dacă nu putem găsi un astfel de articol, ca în acțiunea `UPDATE_INSTALLED`, pur și simplu mergem mai departe fără să atingem nimic altceva. Funcția „produce” se pretează și la curry. Transmiterea unui apel invers ca prim argument pentru „produce” este destinată a fi folosită pentru curry. Semnătura `produsului` cu curry este
 //curried produce signature produce(callback) => (state) => nextState
Să vedem cum ne putem actualiza starea anterioară cu un produs curry. Produsele noastre cu curry ar arăta astfel:
 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; } });

Funcția de produs curry acceptă o funcție ca prim argument și returnează un produs de curry care abia acum necesită o stare din care să producă următoarea stare. Primul argument al funcției este starea draft (care va fi derivată din starea care urmează să fie transmisă la apelarea acestui produs curry). Apoi urmează fiecare număr de argumente pe care dorim să-i transmitem funcției.

Tot ce trebuie să facem acum pentru a folosi această funcție este să trecem în starea din care dorim să producem următoarea stare și obiectul de acțiune așa.

 // 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, });

Rețineți că, într-o aplicație React, când folosiți cârligul useReducer , nu trebuie să transmitem starea în mod explicit, așa cum am făcut mai sus, deoarece se ocupă de asta.

S-ar putea să vă întrebați dacă Immer va primi un hook , ca tot în React în zilele noastre? Ei bine, ești în companie cu vești bune. Immer are două cârlige pentru lucrul cu state: useImmer și useImmerReducer . Să vedem cum funcționează.

Utilizarea useImmer și useImmerReducer

Cea mai bună descriere a cârligului useImmer vine de la use-immer README în sine.

useImmer(initialState) este foarte asemănător cu useState . Funcția returnează un tuplu, prima valoare a tuplului este starea curentă, a doua este funcția de actualizare, care acceptă o funcție de producător immer, în care draft poate fi mutată liber, până când producătorul se termină și se vor face modificări. imuabil și devin următoarea stare.

Pentru a folosi aceste cârlige, trebuie să le instalați separat, pe lângă biblioteca principală Immer.

 yarn add immer use-immer

În termeni de cod, cârligul useImmer arată ca mai jos

 import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)

Și este la fel de simplu. Ai putea spune că este useState de la React, dar cu puțin steroizi. Utilizarea funcției de actualizare este foarte simplă. Primește starea draft și o poți modifica cât dorești ca mai jos.

 // make changes to data updateData(draft => { // modify the draft as much as you want. })

Creatorul Immer a oferit un exemplu de coduri și casete de coduri cu care vă puteți juca pentru a vedea cum funcționează.

useImmerReducer este la fel de simplu de utilizat dacă ați folosit cârligul useReducer de la React. Are o semnătură similară. Să vedem cum arată în termeni de cod.

 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);

Putem vedea că reductorul primește o stare draft pe care o putem modifica cât vrem. Există, de asemenea, un exemplu de coduri și casete de coduri, pe care să îl experimentați.

Și așa de simplu este să folosești cârlige Immer. Dar în cazul în care încă vă întrebați de ce ar trebui să utilizați Immer în proiectul dvs., iată un rezumat al unora dintre cele mai importante motive pe care le-am găsit pentru a utiliza Immer.

De ce ar trebui să utilizați Immer

Dacă ați scris logica de gestionare a stării pentru o perioadă de timp, veți aprecia rapid simplitatea oferită de Immer. Dar acesta nu este singurul beneficiu oferit de Immer.

Când utilizați Immer, ajungeți să scrieți mai puțin cod boilerplate, așa cum am văzut cu reductoare relativ simple. Acest lucru face, de asemenea, actualizările profunde relativ ușoare.

Cu biblioteci precum Immutable.js, trebuie să înveți un nou API pentru a profita de beneficiile imuabilității. Dar cu Immer, obțineți același lucru cu Objects , Arrays , Sets și Maps JavaScript normale. Nu este nimic nou de învățat.

Immer oferă, de asemenea, partajarea structurală în mod implicit. Aceasta înseamnă pur și simplu că atunci când faceți modificări la un obiect de stare, Immer partajează automat părțile neschimbate ale stării între noua stare și starea anterioară.

Cu Immer, obțineți și înghețarea automată a obiectelor, ceea ce înseamnă că nu puteți modifica starea produced . De exemplu, când am început să folosesc Immer, am încercat să aplic metoda de sort pe o serie de obiecte returnate de funcția produce a lui Immer. A generat o eroare care îmi spunea că nu pot face nicio modificare în matrice. A trebuit să aplic metoda array slice înainte de a aplica sort . Încă o dată, nextState produs este un arbore de stări imuabil.

Immer este, de asemenea, puternic tastat și foarte mic, la doar 3KB atunci când este gzipped.

Concluzie

Când vine vorba de gestionarea actualizărilor de stare, utilizarea Immer este o idee simplă pentru mine. Este o bibliotecă foarte ușoară, care vă permite să continuați să utilizați toate lucrurile pe care le-ați învățat despre JavaScript fără a încerca să învățați ceva complet nou. Vă încurajez să îl instalați în proiectul dvs. și să începeți să îl utilizați imediat. Puteți adăuga utilizarea în proiectele existente și actualizați treptat reductoarele.

De asemenea, vă încurajez să citiți articolul introductiv pe blogul Immer de Michael Weststrate. Partea pe care o găsesc deosebit de interesantă este „Cum funcționează Immer?” secțiune care explică modul în care Immer folosește funcțiile limbajului, cum ar fi proxy-urile și concepte precum copierea la scriere.

De asemenea, vă încurajez să aruncați o privire la această postare de blog: Imutability in JavaScript: A Contratian View unde autorul, Steven de Salas, își prezintă gândurile despre meritele urmăririi imuabilității.

Sper că, cu lucrurile pe care le-ați învățat în această postare, puteți începe să utilizați Immer imediat.

Resurse conexe

  1. use-immer , GitHub
  2. Immer, GitHub
  3. function , MDN web docs, Mozilla
  4. proxy , documente web MDN, Mozilla
  5. Obiect (informatica), Wikipedia
  6. „Imuabilitatea în JS”, ​​Orji Chidi Matthew, GitHub
  7. „Tipuri și valori de date ECMAScript”, Ecma International
  8. Colecții imuabile pentru JavaScript, Immutable.js, GitHub
  9. „Cazul pentru imutabilitate”, Immutable.js, GitHub