Recoil būsenos valdymas React

React ekosistemoje būsenos valdymas visada buvo karštų diskusijų tema. Nuo Redux iki MobX, nuo Context API iki Zustand – kiekvienas sprendimas turi savo šalininkų ir kritikų. Tačiau kai Facebook komanda 2020 metais pristatė Recoil, daugelis kūrėjų pajuto, kad tai gali būti tas trūkstamas gabalas, kuris natūraliai integruojasi į React filosofiją.

Recoil atsirado ne iš tuščios vietos. Jis gimė iš realių Facebook produktų poreikių, kur sudėtinga būsena turėjo būti valdoma efektyviai, tačiau be papildomo boilerplate kodo kalnų. Ir štai čia prasideda įdomi kelionė į tai, kaip Recoil keičia žaidimo taisykles.

Kodėl dar vienas būsenos valdymo įrankis?

Pirmiausia kyla klausimas – ar mums tikrai reikėjo dar vieno būsenos valdymo sprendimo? Juk turime Redux, kuris veikia puikiai, turime Context API, kuris integruotas į React. Atsakymas slypi ne tame, kad kiti sprendimai blogi, o tame, kad Recoil siūlo kitokį požiūrį.

Redux yra nuostabus, kai reikia centralizuotos būsenos su griežta duomenų srautu. Bet būkime sąžiningi – daugeliui projektų tai yra per daug. Reducers, actions, action creators, middleware – visa tai veikia, bet kartais jaučiasi kaip patranką šaudyti į žvirblius. Context API yra paprastesnis, bet turi savo problemų su performance, ypač kai konteksto reikšmė keičiasi dažnai.

Recoil užima įdomią nišą. Jis sukurtas taip, kad jaustųsi kaip natūrali React dalis. Jei mokate naudoti useState, jau suprasite 80% Recoil logikos. Tai nėra atsitiktinumas – Recoil dizainas sąmoningai seka React Hooks API stilių.

Atoms ir Selectors – pagrindiniai statybiniai blokai

Recoil pasaulyje viskas sukasi apie du pagrindinius konceptus: atoms ir selectors. Atoms yra būsenos vienetai, o selectors – tai funkcijos, kurios transformuoja ar kombinuoja tą būseną.

Atom yra paprasčiausias dalykas, kokį galite įsivaizduoti. Tai tiesiog būsenos gabalas, kurį galite skaityti ir rašyti iš bet kurio komponento. Štai kaip tai atrodo praktikoje:

import { atom } from 'recoil';

const userNameState = atom({
  key: 'userName',
  default: ''
});

Kiekvienas atom turi unikalų raktą (key) ir numatytąją reikšmę. Raktas yra svarbus – Recoil naudoja jį vidiniam būsenos valdymui ir debug’inimui. Rekomenduoju naudoti aprašomus pavadinimus, kurie atskleidžia, ką atom’as saugo.

Selectors yra kur kas įdomesni. Jie leidžia jums sukurti išvestinę būseną, kuri automatiškai perskaičiuojama, kai pasikeičia jos priklausomybės. Tai panašu į computed properties Vue ar useMemo React, tik daug galingesnis:

import { selector } from 'recoil';

const userGreetingState = selector({
  key: 'userGreeting',
  get: ({get}) => {
    const name = get(userNameState);
    return name ? `Sveiki, ${name}!` : 'Sveiki, svečias!';
  }
});

Selector automatiškai sekasi userNameState pakitimus ir perskaičiuoja savo reikšmę. Jūs neturite rūpintis prenumeratomis ar atsisakymais – Recoil viską tvarko už jus.

Kaip tai veikia komponentuose

Teorija teorija, bet kaip visa tai atrodo realiame kode? Čia Recoil tikrai spindi. Norėdami naudoti atom’ą komponente, tiesiog naudojate useRecoilState hook’ą, kuris veikia beveik identiškai kaip useState:

import { useRecoilState } from 'recoil';

function UserProfile() {
  const [userName, setUserName] = useRecoilState(userNameState);
  
  return (
    
setUserName(e.target.value)} />
); }

Jei jums reikia tik skaityti būseną, naudokite useRecoilValue. Jei tik rašyti – useSetRecoilState. Ši API paprastumas yra viena didžiausių Recoil stiprybių. Nereikia mokytis naujos paradigmos ar keisti mąstymo būdo.

Bet štai kur tampa įdomu – keli komponentai gali naudoti tą patį atom’ą, ir jie visi automatiškai sinchronizuojasi. Kai vienas komponentas pakeičia reikšmę, visi kiti, kurie naudoja tą atom’ą, automatiškai atsinaujina. Jokio prop drilling, jokių kontekstų perdavimo per dešimt komponentų lygių.

Asinchroninė būsena ir duomenų gavimas

Vienas iš labiausiai neįvertintų Recoil aspektų yra tai, kaip jis tvarko asinchronines operacijas. Daugelis būsenos valdymo bibliotekų reikalauja papildomų įrankių ar middleware duomenų gavimui iš API. Recoil tai palaiko iš dėžės.

Galite sukurti selector’ių, kuris grąžina Promise, ir Recoil automatiškai tvarkys loading būsenas:

const currentUserState = selector({
  key: 'currentUser',
  get: async ({get}) => {
    const userId = get(userIdState);
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
});

Komponente naudojate Suspense ir ErrorBoundary, kad tvarkytumėte loading ir error būsenas:

function UserData() {
  const user = useRecoilValue(currentUserState);
  return 
{user.name}
; } function App() { return ( Kraunama...
}> ); }

Tai gali atrodyti keista iš pradžių, ypač jei esate įpratę prie tradicinio loading flag požiūrio. Bet kai pripranti, tai tampa labai elegantišku būdu tvarkyti asinchronines operacijas. Komponentas nežino ir nesirūpina, ar duomenys yra sinchroniniai ar asinchroniniai – jis tiesiog gauna reikšmę.

Atomų šeimos ir dinaminė būsena

Kartais jums reikia dinamiškai kurti būsenos gabalus. Pavyzdžiui, turite sąrašą elementų, ir kiekvienas elementas turi savo būseną. Čia į žaidimą įsijungia atom families.

Atom family yra funkcija, kuri grąžina atom’ą pagal parametrą. Tai leidžia jums turėti daugybę susijusių atom’ų be rankinio jų kūrimo:

const todoItemState = atomFamily({
  key: 'todoItem',
  default: (id) => ({
    id,
    text: '',
    completed: false
  })
});

Dabar galite pasiekti konkretų todo elementą pagal jo ID:

function TodoItem({id}) {
  const [todo, setTodo] = useRecoilState(todoItemState(id));
  
  return (
    
setTodo({...todo, text: e.target.value})} />
); }

Tai neįtikėtinai galinga koncepcija. Galite turėti šimtus ar tūkstančius atom’ų, ir Recoil efektyviai tvarkys jų atminties valdymą. Nenaudojami atom’ai automatiškai išvalomi, todėl nereikia rūpintis memory leaks.

Performance optimizacijos ir best practices

Recoil yra greitas iš prigimties, bet yra keletas dalykų, kuriuos turėtumėte žinoti, kad išspausti maksimalų našumą.

Pirma, atom’ai ir selector’iai yra lazy. Tai reiškia, kad jie neskaičiuojami, kol kas nors jų neprašo. Tai automatiškai optimizuoja jūsų aplikaciją – nereikalingi skaičiavimai tiesiog nevyksta.

Antra, komponentai re-renderinasi tik tada, kai pasikeičia jų naudojami atom’ai ar selector’iai. Jei turite didelį objektą atom’e, bet komponentas naudoja tik vieną lauką, jis vis tiek re-renderinsis, kai pasikeičia bet kuris objekto laukas. Sprendimas – skaidykite būseną į smulkesnius atom’us arba naudokite selector’ius, kad ištrauktumėte tik reikalingus laukus:

const userEmailState = selector({
  key: 'userEmail',
  get: ({get}) => {
    const user = get(userState);
    return user.email;
  }
});

Trečia, būkite atsargūs su asinchroniniais selector’iais. Jei selector’ius dažnai perskaičiuojamas ir kiekvieną kartą daro API užklausą, galite greitai užkrauti serverį. Naudokite caching strategijas arba debouncing, kai reikia.

Dar vienas patarimas – naudokite Recoil DevTools. Tai browser extension, kuris leidžia jums matyti visą atom’ų medį, jų reikšmes ir kaip jos keičiasi laike. Tai neįkainojamas įrankis debuginimui ir būsenos valdymo supratimui.

Kada Recoil yra geriausias pasirinkimas

Recoil nėra sidabrinis kulka, ir ne kiekvienam projektui jis bus geriausias pasirinkimas. Bet yra scenarijai, kur jis tikrai spindi.

Jei kuriate aplikaciją su daug tarpusavyje susijusios būsenos, kur skirtingi komponentai turi dalintis duomenimis, bet nėra aiškios hierarchijos – Recoil puikus. Pavyzdžiui, dashboard aplikacijos, kur įvairūs widgetai rodo skirtingus duomenų vaizdus, bet visi jie turi reaguoti į vartotojo filtrus ar pasirinkimus.

Jei jūsų aplikacija turi daug išvestinės būsenos, kuri priklauso nuo kitų būsenų – selector’iai čia tikrai padės. Jie automatiškai tvarko priklausomybes ir perskaičiavimus, todėl jums nereikia rašyti daug useEffect hook’ų su sudėtingomis priklausomybių masyvais.

Jei dirbate su React Concurrent Mode arba planuojate jį naudoti ateityje – Recoil buvo sukurtas su tuo omenyje. Jis puikiai veikia su Suspense ir kitomis naujomis React funkcijomis.

Tačiau jei kuriate paprastą aplikaciją su minimalia būsena, kur useState ir props drilling puikiai veikia – galbūt Recoil bus per daug. Jei jau turite gerai veikiančią Redux setup ir komanda ją gerai išmano – migracija į Recoil gali neapsimokėti.

Kelias į priekį su Recoil

Recoil vis dar yra gana jaunas projektas, ir jo ekosistema auga. Facebook aktyviai jį plėtoja, bet reikia pripažinti, kad jis dar nėra pasiekęs 1.0 versijos. Tai nereiškia, kad negalite jo naudoti production’e – daugelis kompanijų, įskaitant Facebook, jau tai daro.

Bendruomenė kuria įdomių įrankių ir bibliotekų aplink Recoil. Yra persistence sprendimų, kurie leidžia išsaugoti būseną localStorage ar IndexedDB. Yra testing utilities, kurie palengvina komponentų su Recoil testavimą. Yra net TypeScript tipų generatoriai, kurie automatiškai sukuria tipus jūsų atom’ams ir selector’iams.

Jei norite pradėti su Recoil, rekomenduoju pradėti nuo mažo – paimkite vieną būsenos gabalą, kurį šiuo metu valdote su Context API ar prop drilling, ir pabandykite jį perkelti į Recoil. Pamatysite, kaip tai supaprastina kodą ir pagerina komponentų struktūrą. Paskui galite palaipsniui plėsti naudojimą į kitas aplikacijos dalis.

Dokumentacija yra gana gera, nors kartais trūksta gilesnių pavyzdžių sudėtingesnėms situacijoms. Bet bendruomenė aktyvi, ir Stack Overflow bei GitHub discussions paprastai turi atsakymus į dažniausias problemas. Verta prisijungti prie Recoil Discord serverio, kur galite gauti pagalbos ir pasidalinti patirtimi su kitais kūrėjais.

Recoil atveria duris naujam būdui galvoti apie būsenos valdymą React aplikacijose. Jis nėra revoliucija, bet evoliucija – natūralus žingsnis į priekį, kuris išlaiko React dvasią ir paprastumą, tuo pačiu suteikdamas galingus įrankius sudėtingos būsenos valdymui. Ar tai taps nauju standartu? Laikas parodys. Bet viena aišku – Recoil jau dabar yra vertas jūsų dėmesio ir eksperimentavimo.

Daugiau

Python type hinting: statinis tipų tikrinimas