De ce ar trebui să alegeți useState în loc de useReducer
⏱️ 13 min read
de Austin Malerba
Table of Contents
De ce ar trebui să alegeți useState în loc de useReducer
Un ghid pentru gestionarea statului local și global prin useState
De la introducerea API-ului React Hooks, am văzut multe discuții despre useState, useReducer, și când să folosiți una peste alta. Din aceste conversații, s-ar ajunge la concluzia că useState este cel mai potrivit pentru starea simplă cu logică simplă de actualizare și asta useReducer este cel mai bun pentru forme complexe de stare cu logică de actualizare complexă.
Sunt aici pentru a vă convinge că când este înfășurat într-un cârlig personalizat de 4 linii, useState poate fi la fel de puternic ca, dacă nu chiar mai puternic decât, useReducer la gestionarea stării complexe.
Nu-mi plac reductoarele. Am încercat să le folosesc, dar ajung mereu să plec. Ceva se simte în neregulă cu privire la trimiterea acțiunilor pentru a declanșa logica de afaceri atunci când aș putea face acest lucru invocând o funcție cu argumente.
Și apoi există faptul că, în loc să încapsulez logica mea de afaceri în funcții, ar trebui să le grupez pe toate într-o funcție gigantă partiționată de o grămadă de cazuri de comutare? Am încercat biblioteci precum acțiuni redux pentru a atenua această îngrijorare, dar încă nu m-am putut descurca. Antipatia mea pentru reductoare m-a motivat să caut o soluție mai bună.
Să analizăm câteva motive obișnuite pentru care oamenii aleg useReducer peste useState:
Deci logica de afaceri poate fi centralizată în reductor, spre deosebire de împrăștiată în jurul componentei
Reductoarele sunt funcții pure care sunt ușor de testat izolat de React
Reductoarele permit actualizarea predictibilă a unor bucăți de stare care depind unele de altele (în timp ce multiple useStates-ar putea să nu)
Dacă oricare dintre aceste gloanțe este confuz, aș recomanda să aruncați o privire la asta articol. De-a lungul acestui ghid, mă voi referi la aceste elemente ca fiind cele trei avantaje ale reductoarelor.
Primul pas: Construirea unui exemplu
Mai întâi, vă voi arăta un exemplu care prezintă beneficiile reductoarelor pe care le-am menționat mai sus și apoi vă voi arăta cum puteți implementa aceeași funcționalitate prin useState fără a sacrifica vreunul dintre beneficiile unei useReducer soluţie.
Pentru a ilustra avantajele / dezavantajele useState vs. useReducer Voi implementa un contor simplu cu o întorsătură. Contorul poate fi incrementat, dar poate fi și înghețat. Dacă este în stare înghețată, incrementarea contorului nu va face nimic.
După cum puteți vedea, am implementat o dată contorul nostru de mai sus useState și odată cu useReducer. In orice caz, StateCounterV1 are unele probleme. De fapt, nici măcar nu funcționează așa cum era de așteptat.
Ne-am aștepta la asta StateCounterV1 ar trebui să redea <div>1
deoarece incrementăm contorul o dată, apoi înghețăm contorul și apoi incrementăm din nou. Dar in realitate it renders
2
deoarece the second invocarea incrementului nu are access to noua valoare a înghețat. Aceasta illustrates benefit #3 of useReducer peste useState.
De asemenea, este evident că în StateCounterV1 logica noastră de a crește contorul se află în componenta însăși, dar în ReducerCounter logica aparține countReducer (beneficiul nr. 1).
Și, în sfârșit, vedem asta pentru a testa logica numărării în StateCounterV1 ar trebui să o redăm, în timp ce pentru a testa logica în countReducer, am putea face acest lucru fără a fi nevoie să redăm vreodată o componentă. L-am putea testa pur și simplu invocându-l cu o stare și o acțiune și asigurându-ne că produce următoarea stare corectă (beneficiul nr. 2).
Pasul doi: starea de colaps
În exemplul nostru, avem o tranziție de stat, increment, care se actualizează count dar depinde de un alt bucată de stat, frozen. În astfel de cazuri, consider că este mai bine să consolidez starea. În teorie am putea avea întotdeauna maximum unul useState cârlig pe componentă și realizăm în continuare orice funcționalitate dorim. Dar este în regulă useState de mai multe ori atâta timp cât bucățile de stare nu depind unele de altele la actualizare. Acestea fiind spuse, să vedem cum starea consolidată ne poate restitui avantajul nr. 3 al reductoarelor.
Acum actualizatorul a trecut la setState în a noastră increment funcția este autosuficientă. Nu mai trebuie să ajungă frozen prin închidere pentru a determina cum se produce următoarea stare. In schimb prevState conține toată starea necesară pentru a-și efectua logica de actualizare.
Deoarece este autosuficient, nu mai avem nevoie să-l declarăm la momentul redării, am putea să îl scoatem din componentă.
Când ridicăm declarațiile de actualizare a statului în afara componentei noastre, nu numai că îmbunătățim performanța, ci ne împiedicăm să depindem accidental de variabile prin închidere, așa cum am făcut în StateCounterV1. Acest model este puțin în afara punctului acestui articol, dar m-am gândit să îl menționez oricum.
Pasul trei: extragerea logicii de afaceri
In acest punct StateCounterV2 este încă umflat cu contra logică. Dar nu vă faceți griji, tot ce trebuie să facem este să extragem toată logica noastră de afaceri contrară într-un cârlig personalizat. Să-i spunem useCounterApi.
Acum StateCounterV3 arata bine. Aș susține că arată chiar mai bine decât ReducerCounter. Fără a menționa acest refactor a fost simplu, deoarece tot ce a fost nevoie a fost o copiere / lipire a logicii noastre de contor într-un cârlig personalizat. Dar iată că lucrurile devin dificile.
Uneori poate fi greu, ca dezvoltatori, să identificăm unde aparține logica. Creierele noastre sunt neregulate și există câteva zile în care nu mi-ar trece prin cap să extrag această logică din componentă într-un contor personalizat. De aceea, dezvoltatorii avem nevoie de interfețe avizate pentru a ne ghida în direcția corectă.
Pasul patru: Crearea îndrumării
Dacă ar fi să descriem useCounterApi verbal, probabil am spune,
„Este un cârlig personalizat care creează și returnează un contor API.”
Aici se află primul nostru indiciu. Aceasta creează și returnează un API. Astfel, este o fabrică API. Mai precis, este un Tejghea Fabrica API.
Dar ne place să abstractizăm lucrurile, așa că următoarea întrebare este, cum putem face un Generic Fabrica API? Ei bine, să eliminăm partea „Contor” din useCounterApi. Acum am rămas cu useApi. Minunat, acum avem fabrica noastră API generică. Dar unde merge logica noastră de afaceri?
Să ne gândim mai multe despre cum useReducer lucrări.
Primul argument al useReducer este un reductor și al doilea argument este starea inițială. Amintiți-vă că reductorul conține logică de afaceri. Să încercăm să imităm această interfață.
const api = useApi(someApiFactoryFunction, initialArg);
Bine, se pare că ne apropiem de o soluție. Dar acum trebuie să ne dăm seama ce naiba someApiFactoryFunction ar trebui să facă.
Ei bine, știm că ar trebui să conțină logica de afaceri și știm că ar trebui să nu știe de React, astfel încât să îl putem testa fără a fi nevoie să redăm o componentă. Ceea ce știm și noi este că someApiFactoryFunction nu poate conține un useState invocație pentru că atunci ea ar fii conștient de reacția lucrurilor. Dar cu siguranță are nevoie state și setState . Deci va trebui să injectăm state și setState altfel. Deci, cum putem injecta lucrurile din nou în funcții? Da, parametri. Legând împreună acest exercițiu de gândire, ajungem la următoarele.
Și iată-l. useApi este cârligul nostru personalizat cu 4 linii magice care dezvăluie adevărata putere a useState. Funcțiile API Factory ne furnizează curentul state și a setState callback și permiteți-ne să expunem un API de la acestea. Să ne gândim la ce fel de beneficii tocmai am introdus cu această simplă modificare a contractului.
counterApiFactory nu este conștient de React, ceea ce înseamnă că acum îl putem testa pur și simplu trecând un state obiect și a setState callback (Reducerea beneficiului # 2 realizată).
useApi se așteaptă la o fabrică API, ceea ce înseamnă că îi spunem dezvoltatorului că nevoie pentru a scrie funcții API Factory cu semnătura ({state, setState}) => api. Asta înseamnă, chiar și în zilele mele libere, când creierul meu se străduiește să recunoască faptul că un cluster de logică poate fi refactorizat într-un API cu stare, am acest micle useFuncția Api mă determină să arunc toată logica mea de afaceri într-o locație centralizată.
Pasul cinci: Optimizare
Ca atare, useApi nu este la fel de eficient pe cât ar putea fi. Orice componentă care consumă useApi va invoca useApi pe fiecare redare, ceea ce înseamnă apiFactory va fi, de asemenea, invocat la fiecare randare. Nu este necesar să invocăm apiFactory pe fiecare randare, ci mai degrabă numai când state s-a schimbat. Putem optimiza useApi prin memoriul executării apiFactory.
Testarea unei fabrici API
Acum că ne-am implementat useApi cârlig, să ne uităm la modul în care am testa o fabrică API.
Este suficient de simplu pentru a crea un înveliș în jurul nostru counterApiFactory care imită comportamentul state/setState. Cu această funcție de ajutor ne putem testa counterApiFactory într-un mod foarte natural.
useApi vs useReducer
Să comparăm acum aceste două soluții.
Incapsulare logică
În ambele soluții, logica actualizării stării este centralizată, ceea ce permite raționarea ușoară, depanarea și testarea. Cu toate acestea, reductoarele oferă doar un mecanism pentru Actualizați stat, ele nu oferă un mecanism pentru recupera stat. În schimb, este obișnuit să scrieți selectoare și să le aplicați în aval de reductor. Ce e frumos la noi useApi soluția este că încapsulează nu numai logica actualizării stării, ci și logica recupera stat ?.
Actualizarea statului
Pentru a actualiza starea cu useReducer, trebuie să trimitem acțiuni. Pentru a actualiza starea cu useApi trebuie să invocăm metode de actualizare. Un avantaj potențial al reductorilor în acest scenariu este că reductoarele multiple ar putea asculta aceeași acțiune. Cu toate acestea, acest lucru vine și cu un dezavantaj: fluxul de execuție nu este intuitiv odată ce o acțiune a fost expediată. Dacă am nevoie de mai multe bucăți de stare disparate pentru a fi actualizate simultan, aș prefera să o fac în mod explicit cu mai multe apeluri de metodă API back-to-back, decât printr-o singură acțiune expediată care este transmisă tuturor reductorilor.
Performanţă
Un lucru frumos despre reductoare este că, prin compoziția reductorului, reductoarele multiple pot asculta o singură acțiune expediată, ceea ce înseamnă că puteți avea multe părți ale schimbării stării într-o singură redare. Nu am venit cu o soluție pentru compoziția API Factory (deși este cu siguranță posibilă). Deocamdată, soluția mea este să invoc actualizatorii de stare înapoi, când este necesar, ceea ce ar putea duce la mai multe randări decât o abordare de reducere.
Boilerplate
Soluțiile bazate pe reductoare sunt notoriu boilerplate-y (mai ales atunci când se lucrează cu redux). Declarațiile de tip acțiune ocupă un spațiu suplimentar, iar distribuirea acțiunilor tinde să fie ceva mai detaliată decât simpla invocare a unei funcții cu argumente. Din aceste motive aș spune useApi are o ușoară margine pe useReducer în ceea ce privește codul cazanului.
Testabilitate
Atât reductoarele, cât și fabricile API sunt ușor de testat.
Explorarea ulterioară useApi
Să aruncăm o privire la alte lucruri interesante cu care putem face useApi.
Mi-am luat timp să pun în aplicare clasicul Exemplu de listă Redux Todo prin intermediul useApi. Iată cum todosApiFactory arată în implementarea useApi.
Un lucru grosolan pe care l-ați observat în codul de mai sus este repetarea următoarei centrale.
setState(prevState => ({ ...prevState, /* … */});
Presupunând că state este un obiect și pentru că setState nu sustine fuzionare superficială, trebuie să facem acest lucru pentru a ne asigura că păstrăm orice stat cu care nu lucrăm în prezent.
Putem reduce o parte din acest cazan și putem obține alte beneficii interesante dintr-o bibliotecă numită imer. imer este o bibliotecă de imuabilitate care vă permite să scrieți cod imuabil într-un mod mutabil.
După cum puteți vedea, imer ne ajută să eliminăm o parte din acel cod enervant al cazanului necesar atunci când scriem actualizări imuabile. Dar atenție, comoditatea lui imer este și călcâiul lui Ahile. Un dezvoltator care a introdus conceptul de imuabilitate prin imer ar putea să nu înțeleagă pe deplin consecințele mutațiilor.
Dar așteaptă o secundă, useApi oferă doar starea local, cu exceptia Exemplu de listă Todo folosește redux pentru a furniza un global soluție de stat.
Magazine globale cu API Factories
Să vedem cum putem crea magazine globale din fabricile API.
Nu e deloc rău, nu? Contextul face ca statul global să fie foarte ușor în React. Așadar, acum avem o soluție globală de gestionare a statului pe care să o folosim cu API Factories.
Mai jos este un exemplu de lucru API Factory Todo List List.
Concluzie
Pentru a-l încheia, acest articol conține trei funcții pe care le-ați putea găsi utile.
Aceste funcții oferă abstracții utile pentru managementul de stat local și global alimentat de useState.
Nu mă înțelegeți greșit, reductoarele vin cu multe avantaje, dar pur și simplu nu pot sta ușor cu interfața pe care o oferă. Ambii useApi și useReducer ofera solutii viabile managementului complex al statului. Este într-adevăr o chestiune de preferință.
O soluție utilă este că bibliotecile nu trebuie să efectueze o logică complexă pentru a fi utile. O mare parte din bibliotecile și cadrele de valoare oferite nu au legătură cu logica pe care o realizează, ci mai degrabă îndrumările pe care le oferă dezvoltatorului. Bibliotecile / cadrele bune îl obligă pe dezvoltator să urmeze tiparele cunoscute prin interfețe explicite și avizate. useApi face foarte puțin din punct de vedere al calculului, dar încurajează dezvoltatorul să-și plaseze logica de afaceri în stare centralizată, evitând totodată poluarea componentelor.