Håndteringsstat i reaktion: Fire uforanderlige fremgangsmåder at overveje

Måske det mest almindelige forvirringspunkt i React i dag: tilstand.

Forestil dig, at du har en formular til redigering af en bruger. Det er almindeligt at oprette en enkelt ændringshåndterer til at håndtere ændringer i alle formularfelter. Det kan se sådan ud:

updateState(event) { const {name, value} = event.target; let user = this.state.user; // this is a reference, not a copy... user[name] = value; // so this mutates state ? return this.setState({user}); }

Bekymringen er på linje 4. Linje 4 muterer faktisk tilstand, fordi brugervariablen er en henvisning til tilstand. Reaktionstilstand skal behandles som uforanderlig.

Fra React-dokumenterne:

Muter aldrig this.statedirekte, da opkald setState()bagefter kan erstatte den mutation, du lavede. Behandl this.statesom om det var uforanderligt.

Hvorfor?

  1. setState-batcher fungerer bag kulisserne. Dette betyder, at en manuel tilstandsmutation kan tilsidesættes, når setState behandles.
  2. Hvis du erklærer en shouldComponentUpdate-metode, kan du ikke bruge en === lighedskontrol indeni, fordi objektreferencen ikke ændres . Så ovenstående tilgang har også en potentiel præstationspåvirkning.

Bundlinje : Eksemplet ovenfor fungerer ofte okay, men for at undgå kanttilfælde skal du behandle tilstanden som uforanderlig.

Her er fire måder at behandle tilstand som uforanderlig på:

Tilgang # 1: Object.assign

Object.assign opretter en kopi af et objekt. Den første parameter er målet, derefter angiver du en eller flere parametre for egenskaber, du gerne vil tackle. Så at fastsætte eksemplet ovenfor indebærer en simpel ændring til linje 3:

updateState(event) { const {name, value} = event.target; let user = Object.assign({}, this.state.user); user[name] = value; return this.setState({user}); }

På linje 3 siger jeg "Opret et nyt tomt objekt og tilføj alle egenskaberne på this.state.user til det." Dette opretter en separat kopi af brugerobjektet, der er gemt i tilstand. Nu er jeg sikker på at mutere brugerobjektet på linje 4 - det er et helt separat objekt fra objektet i tilstand.

Sørg for at udfylde Object.assign, da det ikke understøttes i IE og ikke transpileres af Babel. Fire muligheder at overveje:

  1. objekt-tildel
  2. MDN dokumenterer
  3. Babel Polyfill
  4. Polyfill.io

Fremgangsmåde nr. 2: Objektspredning

Objektspredning er i øjeblikket en fase 3-funktion og kan transmitteres af Babel. Denne tilgang er mere kortfattet:

updateState(event) { const {name, value} = event.target; let user = {...this.state.user, [name]: value}; this.setState({user}); }

På linje 3 siger jeg “Brug alle egenskaberne på this.state.user til at oprette et nyt objekt, og indstil derefter ejendommen repræsenteret af [navn] til en ny værdi, der er sendt videre til event.target.value”. Så denne tilgang fungerer på samme måde som Object.assign-metoden, men har to fordele:

  1. Ingen polyfyld kræves, da Babel kan transpile
  2. Mere kortfattet

Du kan endda bruge destrukturer og inlining til at gøre dette til en one-liner:

updateState({target}) { this.setState({user: {...this.state.user, [target.name]: target.value}}); }

Jeg ødelægger begivenhed i metodesignaturen for at få en henvisning til event.target. Så erklærer jeg, at tilstanden skal indstilles til en kopi af denne.stat.bruger med den relevante egenskab indstillet til en ny værdi. Jeg kan godt lide, hvor kortfattet dette er. Dette er i øjeblikket min favorit tilgang til at skrive ændringshandlere. ?

Disse to tilgange ovenfor er de mest almindelige og ligetil måder at håndtere uforanderlig tilstand på. Vil du have mere strøm? Tjek de to andre muligheder nedenfor.

Tilgang # 3: Immutability Helper

Immutability-helper er et praktisk bibliotek til at mutere en kopi af data uden at ændre kilden. Dette bibliotek er foreslået i React's dokumenter.

// Import at the top: import update from 'immutability-helper'; updateState({target}) { let user = update(this.state.user, {$merge: {[target.name]: target.value}}); this.setState({user}); }

På linje 5 kalder jeg fusion, som er en af ​​mange kommandoer leveret af uforanderlig hjælper. Meget ligesom Object.assign sender jeg det målobjektet som den første parameter og angiver derefter den ejendom, jeg gerne vil fusionere i.

Der er meget mere ved uforanderlig hjælper end dette. Det bruger en syntaks inspireret af MongoDBs forespørgselssprog og tilbyder en række effektive måder at arbejde med uforanderlige data på.

Tilgang # 4: Immutable.js

Vil du programmatisk håndhæve uforanderlighed? Overvej immutable.js. Dette bibliotek giver uforanderlige datastrukturer.

Her er et eksempel på et uforanderligt kort:

 // At top, import immutable import { Map } from 'immutable'; // Later, in constructor... this.state = { // Create an immutable map in state using immutable.js user: Map({ firstName: 'Cory', lastName: 'House'}) }; updateState({target}) { // this line returns a new user object assuming an immutable map is stored in state. let user = this.state.user.set(target.name, target.value); this.setState({user}); }

Der er tre grundlæggende trin ovenfor:

  1. Import uforanderlig.
  2. Sæt tilstand til et uforanderligt kort i konstruktøren
  3. Brug den indstillede metode i ændringshåndteringen til at oprette en ny kopi af brugeren.

Skønheden i immutable.js: Hvis du forsøger at mutere tilstand direkte, vil den mislykkes . Med de andre tilgange ovenfor er det let at glemme, og React advarer dig ikke, når du muterer tilstand direkte.

Ulemperne ved uforanderlig?

  1. Oppustethed . Immutable.js tilføjer 57K minificeret til din pakke. I betragtning af biblioteker som Preact kan kun erstatte React i 3K, det er svært at acceptere.
  2. Syntaks . Du skal henvise til objektegenskaber via strenge og metodeopkald i stedet for direkte. Jeg foretrækker bruger.navn fremfor bruger.get ('navn').
  3. YATTL (endnu en ting at lære) - Enhver, der tilmelder sig dit team, skal lære endnu en API til at hente og indstille data samt et nyt sæt datatyper.

Et par andre interessante alternativer at overveje:

  • sømløs-uforanderlig
  • reager-kopi-skriv

Advarsel: Pas på indlejrede genstande!

Mulighed nr. 1 og nr. 2 ovenfor (Object.assign og Object spread) udfører kun en lav klon. Så hvis dit objekt indeholder indlejrede objekter, kopieres de indlejrede objekter ved reference i stedet for efter værdi . Så hvis du ændrer det indlejrede objekt, vil du mutere det originale objekt. ?

Vær kirurgisk om, hvad du kloner. Klon ikke alle tingene. Klon de objekter, der er ændret. Immutability-hjælper (nævnt ovenfor) gør det let. Ligesom alternativer som immer, updeep eller en lang liste over alternativer.

Du kan blive fristet til at nå frem til dybe fusionerende værktøjer som klon-dyb eller lodash.merge, men undgå blind dyb kloning .

Her er hvorfor:

  1. Dyb kloning er dyrt.
  2. Dyb kloning er typisk spildende (i stedet kloner kun det, der rent faktisk har ændret sig)
  3. Dyb kloning forårsager unødvendige gengivelser, da React mener, at alt er ændret, når der faktisk kun er et specifikt børneobjekt, der har ændret sig.

Tak til Dan Abramov for de forslag, jeg har nævnt ovenfor:

Jeg synes ikke, at cloneDeep () er en god anbefaling. Det kan være meget dyrt. Kopier kun de dele, der rent faktisk har ændret sig. Biblioteker som immutability-helper (//t.co/YadMmpiOO8), updeep (//t.co/P0MzD19hcD) eller immer (//t.co/VyRa6Cd4IP) hjælper med dette.

- Dan Abramov (@dan_abramov) 23. april 2018

Sidste tip: Overvej at bruge funktionel setState

En anden rynke kan bide dig:

setState () muterer ikke straks this.state, men skaber en ventende tilstandsovergang. Adgang til this.state efter at have kaldt denne metode kan potentielt returnere den eksisterende værdi.

Da setState-opkald er batchede, fører kode som denne til en fejl:

updateState({target}) { this.setState({user: {...this.state.user, [target.name]: target.value}}); doSomething(this.state.user) // Uh oh, setState merely schedules a state change, so this.state.user may still have old value }

If you want to run code after a setState call has completed, use the callback form of setState:

updateState({target}) { this.setState((prevState) => { const updatedUser = {...prevState.user, [target.name]: target.value}; // use previous value in state to build new state... return { user: updatedUser }; // And what I return here will be set as the new state }, () => this.doSomething(this.state.user); // Now I can safely utilize the new state I've created to call other funcs... ); }

My Take

I admire the simplicity and light weight of option #2 above: Object spread. It doesn’t require a polyfill or separate library, I can declare a change handler on a single line, and I can be surgical about what has changed. ? Working with nested object structures? I currently prefer Immer.

Have other ways you like to handle state in React? Please chime in via the comments!

Looking for More on React? ⚛

I’ve authored multiple React and JavaScript courses on Pluralsight (free trial). My latest, “Creating Reusable React Components” just published! ?

Cory House er forfatter til flere kurser om JavaScript, React, ren kode, .NET og mere om Pluralsight. Han er hovedkonsulent hos reactjsconsulting.com, en softwarearkitekt hos VinSolutions, en Microsoft MVP og træner softwareudviklere internationalt i softwarepraksis som front-end-udvikling og ren kodning. Cory tweets om JavaScript og front-end-udvikling på Twitter som @housecor.