Hvordan man undgår denne React Hooks præstationsgrube

React Hooks lover at undgå omkostningen af ​​klassekomponenter, mens de leverer alle de samme fordele. For eksempel tillader de os at skrive tilstandfulde funktionelle komponenter uden at skulle bekymre os om at gemme tilstand i klasseforekomsten.

Det kræver imidlertid omhu at skrive statefulde komponenter med Hooks. Der er en subtil forskel mellem hvordan tilstand initialiseres i konstruktøren af ​​en klassekomponent og hvordan den initialiseres af useState-krogen. Udviklere, der allerede forstår klassekomponenter og tænker på kroge simpelthen som klassekomponenter uden klassens ting, risikerer at skrive komponenter, der fungerer dårligere end klassekomponenter.

Her diskuterer jeg en funktion ved useState, der kun nævnes kort i de officielle FAQ om Hooks. At forstå denne funktion detaljeret giver dig mulighed for at få mest muligt ud af React Hooks. Ud over at læse denne note inviterer jeg dig til at lege med Stress Testing React Hooks, et benchmarkværktøj, jeg skrev for at illustrere disse særegenheder ved Hooks.

Indstillingerne forud for React Hooks

Antag, at du har nogle dyre beregninger, der skal ske bare en gang, når du opretter din komponent, og antag, at denne beregning afhænger af en eller anden prop. En almindelig funktionel komponent gør et meget dårligt job på dette:

Dette fungerer meget dårligt, fordi den dyre beregning udføres på hver render.

Klassekomponenter forbedrer dette ved at give os mulighed for kun at udføre en given operation, for eksempel i konstruktøren:

Ved at gemme resultatet af beregningen på forekomsten, i dette tilfælde inde i komponentens lokale tilstand, kan vi omgå den dyre beregning på hver efterfølgende gengivelse. Du kan se forskellen, som dette gør ved at sammenligne klassekomponenten og den funktionelle komponent med mit benchmarkværktøj.

Men klassekomponenter har deres egne ulemper, som nævnt i de officielle React Hooks-dokumenter. Derfor blev kroge introduceret.

En naiv implementering med useState

BrugState-krogen kan bruges til at erklære en "tilstandsvariabel" og indstille den til en startværdi. Denne værdi kan ændres og fås adgang til i efterfølgende gengivelser. Med det i tankerne kan du naivt prøve at gøre følgende for at forbedre ydeevnen for din funktionelle komponent:

Du kan måske tro, at da vi har at gøre med tilstand her, der deles mellem efterfølgende render, udføres den dyre beregning kun på den første gengivelse, ligesom med klassekomponenter. Du har forkert.

For at se hvorfor, skal du huske, at NaiveHooksComponent er en bare en funktion, en funktion, der påberåbes på hver gengivelse. Det betyder, at useState kaldes på hver gengivelse. Hvordan useState fungerer er en kompliceret historie, der ikke behøver at bekymre os. Det, der er vigtigt, er, hvad useState påberåbes med: Det påberåbes med returneringsværdien af ​​dýre beregninger. Men vi ved kun, hvad denne returneringsværdi er, hvis vi rent faktisk påberåber dyre beregninger. Som et resultat er vores NaiveHooksComponent dømt til at udføre den dyre beregning på hver render, ligesom vores tidligere FunctionalComponent, der ikke brugte useState.

Indtil videre giver useState ikke nogen ydelsesfordele, som det kan verificeres med mit benchmark-værktøj. (Naturligvis indeholder den matrix, som useState returnerer, også en funktion, der giver os mulighed for let at opdatere tilstandsvariablen, hvilket er noget, vi ikke kunne gøre med en simpel funktionel komponent.)

Tre måder at huske dyre beregninger på

Heldigvis giver React Hooks os tre muligheder til at håndtere tilstand, der er lige så præstationer som klassekomponenter.

1. useMemo

Den første mulighed er at bruge useMemo-krogen:

Som tommelfingerregel udfører useMemo kun den dyre beregning igen, hvis værdien af ​​arg ændres. Dette er dog kun en tommelfingerregel, da fremtidige versioner af React lejlighedsvist kan beregne den memiserede værdi lejlighedsvist.

De næste to indstillinger er mere pålidelige.

2. Videresendelse af funktioner til useState

Den anden mulighed er at videregive en funktion til useState:

Denne funktion kaldes kun på den første gengivelse. Det er super nyttigt. (Selvom du er nødt til at huske, at hvis du vil gemme en faktisk funktion i tilstand, skal du pakke den ind i en anden funktion. Ellers ender du med at gemme funktionens returværdi i stedet for selve funktionen.)

3. useRef

Den tredje mulighed er at bruge useRef-krog:

Denne er lidt underlig, men den fungerer, og den er officielt sanktioneret. useRef returnerer et mutérbart ref-objekt, hvis nuværende nøgle peger på det argument, som useRef påberopes med. Dette ref-objekt vil fortsætte i efterfølgende gengivelser. Så hvis vi indstiller aktuelle dovne som vi gør ovenfor, udføres den dyre beregning kun én gang.

Sammenligning

Som du kan se med mit benchmark-værktøj, er alle disse tre muligheder lige så performante som vores første klassekomponent. Brugsmemos opførsel kan dog ændre sig i fremtiden. Så hvis du vil have en garanti for, at den dyre beregning kun udføres en gang, skal du enten bruge mulighed 2, som videresender en funktion til useState, eller mulighed 3, der bruger useRef.

Valget mellem disse to indstillinger kommer til, om du nogensinde vil opdatere resultatet af den dyre beregning. Tænk på forskellen mellem mulighed 2 og mulighed 3 som analog med forskellen mellem at opbevare noget i dette. Stat eller opbevare det direkte i dette.