Ascensiunea mașinilor de stat

Publicat: 2022-03-10
Rezumat rapid ↬ Dezvoltarea interfeței de utilizare a devenit dificilă în ultimii doi ani. Asta pentru că am împins gestionarea stării în browser. Și gestionarea statului este ceea ce face ca munca noastră să fie o provocare. Dacă o facem corect, vom vedea cum aplicația noastră se scalează ușor, fără erori. În acest articol, vom vedea cum să folosim conceptul de mașină de stat pentru rezolvarea problemelor de management de stat.

Este deja 2018 și nenumărați dezvoltatori front-end încă conduc o luptă împotriva complexității și imobilității. Lună după lună, au căutat Sfântul Graal: o arhitectură a aplicației fără erori care îi va ajuta să livreze rapid și cu o calitate înaltă. Sunt unul dintre acești dezvoltatori și am găsit ceva interesant care ar putea ajuta.

Am făcut un pas bun înainte cu instrumente precum React și Redux. Cu toate acestea, ele nu sunt suficiente singure în aplicații la scară largă. Acest articol vă va prezenta conceptul de mașini de stat în contextul dezvoltării front-end. Probabil că ați construit deja câteva dintre ele fără să vă dați seama.

O introducere în mașinile de stat

O mașină de stări este un model matematic de calcul. Este un concept abstract prin care mașina poate avea diferite stări, dar la un moment dat îndeplinește doar una dintre ele. Există diferite tipuri de mașini de stat. Cel mai faimos, cred, este mașina Turing. Este o mașină de stări infinite, ceea ce înseamnă că poate avea un număr nenumărat de stări. Mașina Turing nu se potrivește bine în dezvoltarea UI de astăzi, deoarece în cele mai multe cazuri avem un număr finit de stări. Acesta este motivul pentru care mașinile cu stări finite, precum Mealy și Moore, au mai mult sens.

Diferența dintre ele este că mașina Moore își schimbă starea doar pe baza stării anterioare. Din păcate, avem o mulțime de factori externi, cum ar fi interacțiunile utilizatorilor și procesele de rețea, ceea ce înseamnă că nici mașina Moore nu este suficient de bună pentru noi. Ceea ce căutăm este mașina Mealy. Are o stare inițială și apoi trece la stări noi pe baza intrării și a stării sale curente.

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

Una dintre cele mai simple moduri de a ilustra modul în care funcționează o mașină de stat este să te uiți la un turnichet. Are un număr finit de stări: blocat și deblocat. Iată un grafic simplu care ne arată aceste stări, cu posibilele lor intrări și tranziții.

turnichet

Starea inițială a turnichetului este blocată. Indiferent de câte ori l-am împinge, rămâne în acea stare blocată. Cu toate acestea, dacă îi transmitem o monedă, atunci aceasta trece la starea deblocată. O altă monedă în acest moment nu ar face nimic; ar fi încă în starea deblocat. O împingere din cealaltă parte ar funcționa și am putea trece. Această acțiune face, de asemenea, tranziția mașinii la starea inițială blocată.

Dacă am dori să implementăm o singură funcție care să controleze turnichetul, probabil am ajunge la două argumente: starea curentă și o acțiune. Și dacă utilizați Redux, probabil că acest lucru vă sună familiar. Este similară cu binecunoscuta funcție de reducere, în care primim starea curentă și, pe baza sarcinii utile a acțiunii, decidem care va fi următoarea stare. Reductorul este tranziția în contextul mașinilor de stat. De fapt, orice aplicație care are o stare pe care o putem schimba cumva poate fi numită o mașină de stări. Doar că implementăm totul manual iar și iar.

Cum este mai bună o mașină de stat?

La serviciu, folosim Redux și sunt destul de mulțumit de el. Totuși, am început să văd modele care nu-mi plac. Prin „nu-mi place”, nu vreau să spun că nu funcționează. Mai mult, adaugă complexitate și mă obligă să scriu mai mult cod. A trebuit să întreprind un proiect secundar în care aveam loc de experimentat și am decis să regândesc practicile noastre de dezvoltare React și Redux. Am început să fac notițe despre lucrurile care mă preocupau și mi-am dat seama că o abstractizare a mașinii de stări va rezolva cu adevărat unele dintre aceste probleme. Să intrăm și să vedem cum să implementăm o mașină de stări în JavaScript.

Vom ataca o problemă simplă. Dorim să preluăm date dintr-un API back-end și să le afișăm utilizatorului. Primul pas este să înveți cum să gândești în stări, mai degrabă decât în ​​tranziții. Înainte de a intra în mașinile de stat, fluxul meu de lucru pentru construirea unei astfel de caracteristici arăta cam așa:

  • Afișăm un buton de preluare a datelor.
  • Utilizatorul face clic pe butonul de preluare a datelor.
  • Lansați cererea către back-end.
  • Preluați datele și analizați-le.
  • Arată-l utilizatorului.
  • Sau, dacă există o eroare, afișați mesajul de eroare și afișați butonul de preluare a datelor, astfel încât să putem declanșa din nou procesul.
gândire liniară

Gândim liniar și, practic, încercăm să acoperim toate direcțiile posibile către rezultatul final. Un pas duce la altul și rapid am începe să ne ramificăm codul. Ce zici de probleme cum ar fi dacă utilizatorul dă dublu clic pe buton sau utilizatorul dă clic pe butonul în timp ce așteptăm răspunsul backend-ului sau cererea reușește, dar datele sunt corupte. În aceste cazuri, probabil că am avea diverse steaguri care ne arată ce s-a întâmplat. A avea steaguri înseamnă mai multe clauze if și, în aplicațiile mai complexe, mai multe conflicte.

gândire liniară

Asta pentru că ne gândim la tranziții. Ne concentrăm asupra modului în care se întâmplă aceste tranziții și în ce ordine. În schimb, concentrarea asupra diferitelor stări ale aplicației ar fi mult mai simplă. Câte state avem și care sunt posibilele lor intrări? Folosind același exemplu:

  • inactiv
    În această stare, afișăm butonul fetch-data, stăm și așteptăm. Acțiunea posibilă este:
    • clic
      Când utilizatorul face clic pe buton, lansăm cererea către back-end și apoi trecem mașina la starea de „preluare”.
  • aducerea
    Solicitarea este în zbor și stăm și așteptăm. Actiunile sunt:
    • succes
      Datele ajung cu succes și nu sunt corupte. Folosim datele într-un fel și trecem înapoi la starea „inactiv”.
    • eșec
      Dacă există o eroare la efectuarea cererii sau la analizarea datelor, trecem la starea „eroare”.
  • eroare
    Afișăm un mesaj de eroare și afișăm butonul de preluare a datelor. Această stare acceptă o singură acțiune:
    • reîncercați
      Când utilizatorul face clic pe butonul de reîncercare, lansăm din nou cererea și trecem mașina la starea „preluare”.

Am descris aproximativ aceleași procese, dar cu stări și intrări.

mașină de stat

Acest lucru simplifică logica și o face mai previzibilă. De asemenea, rezolvă unele dintre problemele menționate mai sus. Observați că, în timp ce suntem în starea de „preluare”, nu acceptăm niciun clic. Deci, chiar dacă utilizatorul face clic pe buton, nu se va întâmpla nimic deoarece mașina nu este configurată să răspundă la acțiunea respectivă în acea stare. Această abordare elimină automat ramificarea imprevizibilă a logicii codului nostru. Aceasta înseamnă că vom avea mai puțin cod de acoperit în timpul testării . De asemenea, unele tipuri de testare, cum ar fi testarea de integrare, pot fi automatizate. Gândiți-vă cum am avea o idee cu adevărat clară despre ceea ce face aplicația noastră și am putea crea un script care trece peste stările și tranzițiile definite și care generează aserțiuni. Aceste afirmații ar putea dovedi că am atins toate stările posibile sau am acoperit o anumită călătorie.

De fapt, notarea tuturor stărilor posibile este mai ușoară decât a scrie toate tranzițiile posibile, deoarece știm ce stări avem nevoie sau avem. Apropo, în cele mai multe cazuri, statele ar descrie logica de afaceri a aplicației noastre, în timp ce tranzițiile sunt foarte adesea necunoscute la început. Erorile din software-ul nostru sunt rezultatul acțiunilor trimise într-o stare greșită și/sau la momentul nepotrivit. Ne lasă aplicația într-o stare despre care nu știm, iar acest lucru ne întrerupe programul sau îl face să se comporte incorect. Desigur, nu vrem să fim într-o astfel de situație. Mașinile de stat sunt firewall-uri bune . Ele ne protejează de a ajunge la stări necunoscute, deoarece stabilim limite pentru ceea ce se poate întâmpla și când, fără a spune în mod explicit cum. Conceptul de mașină de stare se împerechează foarte bine cu un flux de date unidirecțional. Împreună, reduc complexitatea codului și clarifică misterul de unde a apărut un stat.

Crearea unei mașini de stări în JavaScript

Destul de vorbă — hai să vedem un cod. Vom folosi același exemplu. Pe baza listei de mai sus, vom începe cu următoarele:

 const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }

Avem stările ca obiecte și posibilele lor intrări ca funcții. Totuși, starea inițială lipsește. Să schimbăm codul de mai sus cu acesta:

 const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }

Odată ce definim toate stările care au sens pentru noi, suntem gata să trimitem intrarea și să schimbăm starea. Vom face asta folosind cele două metode de ajutor de mai jos:

 const machine = { dispatch(actionName, ...payload) { const actions = this.transitions[this.state]; const action = this.transitions[this.state][actionName]; if (action) { action.apply(machine, ...payload); } }, changeStateTo(newState) { this.state = newState; }, ... }

Funcția de dispatch verifică dacă există o acțiune cu numele dat în tranzițiile stării curente. Dacă da, îl declanșează cu sarcina utilă dată. De asemenea, apelăm handler-ul de action cu machine ca context, astfel încât să putem trimite alte acțiuni cu this.dispatch(<action>) sau să schimbăm starea cu this.changeStateTo(<new state>) .

Urmând călătoria utilizatorului din exemplul nostru, prima acțiune pe care trebuie să o trimitem este să facem click . Iată cum arată gestionarea acelei acțiuni:

 transitions: { 'idle': { click: function () { this.changeStateTo('fetching'); service.getData().then( data => { try { this.dispatch('success', JSON.parse(data)); } catch (error) { this.dispatch('failure', error) } }, error => this.dispatch('failure', error) ); } }, ... } machine.dispatch('click');

Mai întâi schimbăm starea mașinii la fetching . Apoi, declanșăm solicitarea către back-end. Să presupunem că avem un serviciu cu o metodă getData care returnează o promisiune. Odată ce este rezolvată și analiza datelor este OK, trimitem success , dacă nu failure .

Până acum, bine. În continuare, trebuie să implementăm acțiuni și intrări de success și failure în starea de fetching :

 transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }

Observați cum ne-am eliberat creierul de a trebui să ne gândim la procesul anterior. Nu ne pasă de clicurile utilizatorilor sau de ce se întâmplă cu solicitarea HTTP. Știm că aplicația este într-o stare de fetching și ne așteptăm doar la aceste două acțiuni. Este un pic ca și cum ai scrie o nouă logică izolat.

Ultimul bit este starea de error . Ar fi bine dacă am furniza acea logică de reîncercare, astfel încât aplicația să se poată recupera după eșec.

 transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }

Aici trebuie să duplicăm logica pe care am scris-o în gestionarea de click . Pentru a evita acest lucru, ar trebui fie să definim handlerul ca o funcție accesibilă ambelor acțiuni, fie să trecem mai întâi la starea idle și apoi să trimitem manual acțiunea de click .

Un exemplu complet al mașinii de stare de lucru poate fi găsit în Codepen-ul meu.

Gestionarea mașinilor de stat cu o bibliotecă

Modelul mașinii cu stări finite funcționează indiferent dacă folosim React, Vue sau Angular. După cum am văzut în secțiunea anterioară, putem implementa cu ușurință o mașină de stare fără prea multe probleme. Cu toate acestea, uneori, o bibliotecă oferă mai multă flexibilitate. Unele dintre cele bune sunt Machina.js și XState. În acest articol, totuși, vom vorbi despre Stent, biblioteca mea asemănătoare Redux care include conceptul de mașini cu stări finite.

Stent este o implementare a unui container de mașini de stat. Urmează câteva dintre ideile din proiectele Redux și Redux-Saga, dar furnizează, după părerea mea, procese mai simple și fără boilerplate. Este dezvoltat folosind dezvoltarea bazată pe readme și am petrecut literalmente săptămâni doar pe designul API. Pentru că scriam biblioteca, am avut șansa de a remedia problemele pe care le-am întâlnit în timpul utilizării arhitecturilor Redux și Flux.

Crearea de Mașini

În cele mai multe cazuri, aplicațiile noastre acoperă mai multe domenii. Nu putem merge cu o singură mașină. Deci, Stent permite crearea multor mașini:

 import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });

Mai târziu, putem obține acces la aceste mașini folosind metoda Machine.get :

 const machineA = Machine.get('A'); const machineB = Machine.get('B');

Conectarea mașinilor la logica de randare

Redarea în cazul meu se face prin React, dar putem folosi orice altă bibliotecă. Se rezumă la declanșarea unui apel invers în care declanșăm randarea. Una dintre primele caracteristici la care am lucrat a fost funcția de connect :

 import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });

Spunem ce mașini sunt importante pentru noi și le dăm numele. Callback-ul pe care îl trecem la map este declanșat o dată inițial și apoi mai târziu de fiecare dată când starea unora dintre mașini se schimbă. Aici declanșăm redarea. În acest moment, avem acces direct la mașinile conectate, astfel încât să putem prelua starea și metodele curente. Există, de asemenea, mapOnce , pentru a declanșa apelul invers o singură dată și mapSilent , pentru a sări peste acea execuție inițială.

Pentru comoditate, un ajutor este exportat special pentru integrarea React. Este într-adevăr similar cu connect(mapStateToProps) .

 import React from 'react'; import { connect } from 'stent/lib/react'; class TodoList extends React.Component { render() { const { isIdle, todos } = this.props; ... } } // MachineA and MachineB are machines defined // using Machine.create function export default connect(TodoList) .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { isIdle: MachineA.isIdle, todos: MachineB.state.todos });

Stent rulează apelul nostru de cartografiere și se așteaptă să primească un obiect - un obiect care este trimis ca elemente de props la componenta noastră React.

Ce este starea în contextul stentului?

Până acum, statul nostru a fost simple șiruri. Din păcate, în lumea reală, trebuie să păstrăm mai mult decât un șir în stare. Acesta este motivul pentru care starea lui Stent este de fapt un obiect cu proprietăți în interior. Singura proprietate rezervată este name . Toate celelalte sunt date specifice aplicației. De exemplu:

 { name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }

Experiența mea cu Stent de până acum îmi arată că, dacă obiectul de stare devine mai mare, probabil că vom avea nevoie de o altă mașină care să gestioneze acele proprietăți suplimentare. Identificarea diferitelor state durează ceva timp, dar cred că acesta este un mare pas înainte în scrierea unor aplicații mai ușor de gestionat. Este un pic ca și cum ai prezice viitorul și a desenat cadre ale acțiunilor posibile.

Lucrul cu mașina de stat

Similar cu exemplul de la început, trebuie să definim stările posibile (finite) ale mașinii noastre și să descriem intrările posibile:

 import { Machine } from 'stent'; const machine = Machine.create('sprinter', { state: { name: 'idle' }, // initial state transitions: { 'idle': { 'run please': function () { return { name: 'running' }; } }, 'running': { 'stop now': function () { return { name: 'idle' }; } } } });

Avem starea noastră inițială, idle , care acceptă o acțiune de run . Odată ce mașina este în stare de running , putem declanșa acțiunea de stop , ceea ce ne readuce la starea idle .

Probabil vă veți aminti de ajutoarele de dispatch și changeStateTo din implementarea noastră mai devreme. Această bibliotecă oferă aceeași logică, dar este ascunsă în interior și nu trebuie să ne gândim la asta. Pentru comoditate, pe baza proprietății transitions , Stent generează următoarele:

  • metode de ajutor pentru a verifica dacă mașina se află într-o anumită stare — starea idle produce metoda isIdle() , în timp ce pentru running avem isRunning() ;
  • metode de ajutor pentru expedierea acțiunilor: runPlease() și stopNow() .

Deci, în exemplul de mai sus, putem folosi asta:

 machine.isIdle(); // boolean machine.isRunning(); // boolean machine.runPlease(); // fires action machine.stopNow(); // fires action

Combinând metodele generate automat cu funcția utilitar connect , putem închide cercul. O interacțiune cu utilizatorul declanșează intrarea și acțiunea mașinii, care actualizează starea. Din cauza acestei actualizări, funcția de mapare transmisă pentru connect este declanșată și suntem informați despre schimbarea stării. Apoi, redăm din nou.

Gestionare de intrare și acțiune

Probabil cel mai important element este gestionarea acțiunii. Acesta este locul în care scriem cea mai mare parte a logicii aplicației, deoarece răspundem la stările de intrare și modificate. Ceva care îmi place foarte mult în Redux este integrat și aici: imuabilitatea și simplitatea funcției de reducere. Esența instrumentului de gestionare a acțiunii Stent este aceeași. Primește starea curentă și sarcina utilă de acțiune și trebuie să returneze starea nouă. Dacă handlerul nu returnează nimic ( undefined ), atunci starea mașinii rămâne aceeași.

 transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }

Să presupunem că trebuie să preluăm date de la un server la distanță. Lansăm cererea și trecem mașina într-o stare de fetching . Odată ce datele vin din back-end, lansăm o acțiune de success , astfel:

 machine.success({ label: '...' });

Apoi, revenim la o stare idle și păstrăm unele date sub forma matricei todos . Există alte câteva valori posibile de setat ca handler de acțiuni. Primul și cel mai simplu caz este atunci când trecem doar un șir care devine noua stare.

 transitions: { 'idle': { 'run': 'running' } }

Aceasta este o tranziție de la { name: 'idle' } la { name: 'running' } folosind acțiunea run() . Această abordare este utilă atunci când avem tranziții de stare sincrone și nu avem metadate. Deci, dacă menținem altceva în stare, acest tip de tranziție o va elimina. În mod similar, putem trece direct un obiect de stare:

 transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }

Trecem de la editing la idle folosind acțiunea deleteAllTodos .

Am văzut deja handlerul de funcții, iar ultima variantă a handler-ului de acțiuni este o funcție generatoare. Este inspirat din proiectul Redux-Saga și arată astfel:

 import { call } from 'stent/lib/helpers'; Machine.create('app', { 'idle': { 'fetch data': function * (state, payload) { yield { name: 'fetching' } try { const data = yield call(requestToBackend, '/api/todos/', 'POST'); return { name: 'idle', data }; } catch (error) { return { name: 'error', error }; } } } });

Dacă nu aveți experiență cu generatoare, acest lucru s-ar putea să arate un pic enigmatic. Dar generatoarele din JavaScript sunt un instrument puternic. Ni se permite să întrerupem gestionarea acțiunilor, să schimbăm starea de mai multe ori și să gestionăm logica asincronă.

Distracție cu generatoare

Când am fost prezentat pentru prima dată în Redux-Saga, am crezut că este o modalitate prea complicată de a gestiona operațiunile asincrone. De fapt, este o implementare destul de inteligentă a modelului de proiectare a comenzii. Principalul beneficiu al acestui model este că separă invocarea logicii și implementarea ei efectivă.

Cu alte cuvinte, spunem ce vrem, dar nu cum ar trebui să se întâmple. Seria de bloguri a lui Matt Hink m-a ajutat să înțeleg cum sunt implementate saga și recomand cu tărie să o citesc. Am adus aceleași idei în Stent și, în scopul acestui articol, vom spune că, furnizând lucruri, dăm instrucțiuni despre ceea ce ne dorim fără a face acest lucru. Odată ce acțiunea este efectuată, primim controlul înapoi.

În acest moment, câteva lucruri pot fi trimise (cedate):

  • un obiect de stare (sau un șir) pentru schimbarea stării mașinii;
  • un apel al asistentului de call (acceptă o funcție sincronă, care este o funcție care returnează o promisiune sau o altă funcție generatoare) - practic spunem: „Rulați asta pentru mine și dacă este asincron, așteptați. După ce ai terminat, dă-mi rezultatul.”;
  • un apel al asistentului de wait (acceptă un șir reprezentând o altă acțiune); dacă folosim această funcție de utilitate, întrerupem handlerul și așteptăm să fie trimisă o altă acțiune.

Iată o funcție care ilustrează variantele:

 const fireHTTPRequest = function () { return new Promise((resolve, reject) => { // ... }); } ... transitions: { 'idle': { 'fetch data': function * () { yield 'fetching'; // sets the state to { name: 'fetching' } yield { name: 'fetching' }; // same as above // wait for getTheData and checkForErrors actions // to be dispatched const [ data, isError ] = yield wait('get the data', 'check for errors'); // wait for the promise returned by fireHTTPRequest // to be resolved const result = yield call(fireHTTPRequest, '/api/data/users'); return { name: 'finish', users: result }; } } }

După cum putem vedea, codul pare sincron, dar de fapt nu este. Este doar Stent care face partea plictisitoare de a aștepta promisiunea rezolvată sau de a repeta peste alt generator.

Cum îmi rezolvă Stent-ul problemelor Redux

Prea mult cod boilerplate

Arhitectura Redux (și Flux) se bazează pe acțiuni care circulă în sistemul nostru. Când aplicația crește, de obicei ajungem să avem o mulțime de constante și creatori de acțiuni. Aceste două lucruri sunt foarte des în foldere diferite, iar urmărirea execuției codului uneori necesită timp. De asemenea, atunci când adăugăm o funcție nouă, trebuie întotdeauna să ne confruntăm cu un set întreg de acțiuni, ceea ce înseamnă definirea mai multor nume de acțiuni și creatori de acțiuni.

În Stent, nu avem nume de acțiuni, iar biblioteca creează automat creatorii de acțiuni pentru noi:

 const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });

Avem creatorul de acțiuni machine.addTodo definit direct ca o metodă a mașinii. Această abordare a rezolvat și o altă problemă cu care m-am confruntat: găsirea reductorului care răspunde la o anumită acțiune. De obicei, în componentele React, vedem nume de creatori de acțiuni, cum ar fi addTodo ; totuși, la reductoare, lucrăm cu un tip de acțiune care este constantă. Uneori trebuie să sar la codul de creator de acțiuni doar ca să pot vedea tipul exact. Aici, nu avem deloc tipuri.

Schimbări imprevizibile ale stării

În general, Redux face o treabă bună de a gestiona starea într-un mod imuabil. Problema nu este în Redux în sine, ci în faptul că dezvoltatorului i se permite să trimită orice acțiune în orice moment. Dacă spunem că avem o acțiune care aprinde luminile, este OK să declanșăm acea acțiune de două ori la rând? Dacă nu, atunci cum ar trebui să rezolvăm această problemă cu Redux? Ei bine, probabil că am pune un cod în reductor care protejează logica și care verifică dacă luminile sunt deja aprinse - poate o clauză if care verifică starea curentă. Acum întrebarea este, nu este asta dincolo de domeniul de aplicare al reductorului? Ar trebui să știe reductorul despre astfel de carcase de margine?

Ceea ce îmi lipsește în Redux este o modalitate de a opri trimiterea unei acțiuni bazată pe starea curentă a aplicației fără a polua reductorul cu logica condiționată. Și nu vreau să duc această decizie nici la stratul de vizualizare, unde creatorul acțiunii este concediat. Cu Stent, acest lucru se întâmplă automat, deoarece aparatul nu răspunde la acțiunile care nu sunt declarate în starea curentă. De exemplu:

 const machine = Machine.create('app', { state: { name: 'idle' }, transitions: { 'idle': { 'run': 'running', 'jump': 'jumping' }, 'running': { 'stop': 'idle' } } }); // this is fine machine.run(); // This will do nothing because at this point // the machine is in a 'running' state and there is // only 'stop' action there. machine.jump();

Faptul că mașina acceptă doar intrări specifice la un moment dat ne protejează de erori ciudate și face aplicațiile noastre mai previzibile.

State, nu tranziții

Redux, ca și Flux, ne face să gândim în termeni de tranziții. Modelul mental de dezvoltare cu Redux este condus în mare măsură de acțiuni și de modul în care aceste acțiuni transformă starea în reductoarele noastre. Nu este rău, dar am descoperit că are mai mult sens să ne gândim în termeni de state - în ce stări ar putea fi aplicația și cum aceste stări reprezintă cerințele de afaceri.

Concluzie

Conceptul de mașini de stat în programare, în special în dezvoltarea UI, mi-a deschis ochii. Am început să văd mașini de stat peste tot și am o oarecare dorință de a trece mereu la acea paradigmă. Cu siguranță văd beneficiile de a avea stări și tranziții mai strict definite între ele. Caut mereu modalități de a-mi face aplicațiile simple și lizibile. Cred că mașinile de stat sunt un pas în această direcție. Conceptul este simplu și în același timp puternic. Are potențialul de a elimina o mulțime de bug-uri.