Over de auteur
Krasimir Tsonev is een coder met meer dan tien jaar ervaring in webontwikkeling. Auteur van twee boeken over Node.js. Hij werkt als senior front-end developer voor …Meer overKrasimir↬
- 20 min lezen
- UI,Apps,JavaScript
- Opgeslagen voor offline lezen
- Delen op Twitter, LinkedIn
Het is al 2018, en talloze front-end ontwikkelaars leiden nog steeds een strijd tegen complexiteit en immobiliteit. Maand na maand hebben ze gezocht naar de heilige graal: een bugvrije applicatiearchitectuur waarmee ze snel en met hoge kwaliteit kunnen leveren. Ik ben een van die ontwikkelaars, en ik heb iets interessants gevonden dat misschien kan helpen.
We hebben een goede stap voorwaarts gezet met tools als React en Redux. Echter, ze zijn niet genoeg op hun eigen in grootschalige toepassingen. Dit artikel zal je kennis laten maken met het concept van state machines in de context van front-end ontwikkeling. Je hebt er waarschijnlijk al een aantal gebouwd zonder het te beseffen.
An Introduction To State Machines
Een state machine is een wiskundig model van computatie. Het is een abstract concept waarbij de machine verschillende toestanden kan hebben, maar op een gegeven moment slechts één daarvan vervult. Er zijn verschillende types van toestandsmachines. De bekendste is, geloof ik, de Turing machine. Het is een oneindige toestandsmachine, wat betekent dat hij een ontelbaar aantal toestanden kan hebben. De Turing machine past niet goed in de hedendaagse UI ontwikkeling omdat we in de meeste gevallen een eindig aantal toestanden hebben. Daarom zijn eindige toestandsmachines, zoals Mealy en Moore, zinvoller.
Het verschil tussen deze machines is dat de Moore machine zijn toestand alleen verandert op basis van zijn vorige toestand. Helaas hebben wij te maken met veel externe factoren, zoals gebruikersinteracties en netwerkprocessen, waardoor de Moore machine ook voor ons niet goed genoeg is. Waar we naar op zoek zijn is de Mealy machine. Deze heeft een begintoestand en gaat dan over naar nieuwe toestanden op basis van invoer en zijn huidige toestand.
Een van de eenvoudigste manieren om te illustreren hoe een toestandsmachine werkt is te kijken naar een tourniquet. Het heeft een eindig aantal toestanden: vergrendeld en ontgrendeld. Hier is een eenvoudige grafiek die ons deze toestanden toont, met hun mogelijke ingangen en overgangen.
De begintoestand van het tourniquet is vergrendeld. Het maakt niet uit hoe vaak we het indrukken, het blijft in die vergrendelde toestand. Als we er echter een muntje tegenaan gooien, gaat het over in de ontgrendelde toestand. Nog een muntje op dit punt zou niets doen; het zou nog steeds in de ontgrendelde toestand zijn. Een duw van de andere kant zou werken, en we zouden kunnen passeren.
Als we een enkele functie zouden willen implementeren die het tourniquet bedient, zouden we waarschijnlijk uitkomen op twee argumenten: de huidige status en een actie. En als je Redux gebruikt, klinkt dit je waarschijnlijk bekend in de oren. Het is vergelijkbaar met de bekende reducer functie, waarbij we de huidige status ontvangen, en op basis van de payload van de actie beslissen wat de volgende status zal zijn. De reducer is de overgang in de context van toestandsmachines. In feite kan elke toepassing die een toestand heeft die we op een of andere manier kunnen veranderen een toestandsmachine genoemd worden. Het is alleen zo dat we alles steeds weer handmatig implementeren.
Hoe is een state machine beter?
Op mijn werk gebruiken we Redux, en ik ben er best tevreden mee. Maar ik begin patronen te zien die ik niet leuk vind. Met “niet leuk” bedoel ik niet dat ze niet werken. Het is meer dat ze complexiteit toevoegen en me dwingen meer code te schrijven. Ik moest een zijproject uitvoeren waarin ik ruimte had om te experimenteren, en ik besloot om onze React- en Redux-ontwikkelingspraktijken te heroverwegen. Ik begon aantekeningen te maken over de dingen die me zorgen baarden, en ik realiseerde me dat een state machine abstractie een aantal van deze problemen echt zou oplossen. Laten we beginnen en kijken hoe we een state machine in JavaScript kunnen implementeren.
We zullen een eenvoudig probleem aanpakken. We willen gegevens ophalen uit een back-end API en die aan de gebruiker laten zien. De allereerste stap is om te leren denken in toestanden, in plaats van overgangen. Voordat we aan toestandsmachines beginnen, zag mijn workflow voor het bouwen van zo’n functie er ongeveer zo uit:
- We tonen een fetch-data knop.
- De gebruiker klikt op de fetch-data knop.
- Start het verzoek naar de back-end.
- Haal de data op en parseer het.
- Toon het aan de gebruiker.
- Of, als er een fout is, toon de foutmelding en toon de fetch-data knop, zodat we het proces opnieuw kunnen starten.
We denken lineair en proberen in principe alle mogelijke richtingen naar het eindresultaat af te dekken. De ene stap leidt naar de andere, en al snel zouden we onze code gaan vertakken. Hoe zit het met problemen zoals de gebruiker die dubbelklikt op de knop, of de gebruiker die op de knop klikt terwijl we wachten op het antwoord van de back-end, of het verzoek dat slaagt maar de gegevens zijn beschadigd. In deze gevallen zouden we waarschijnlijk verschillende vlaggen hebben die ons laten zien wat er gebeurd is. Het hebben van vlaggen betekent meer if
clausules en, in complexere apps, meer conflicten.
Dit komt omdat we denken in overgangen. We richten ons op hoe deze overgangen gebeuren en in welke volgorde. Het zou een stuk eenvoudiger zijn als we ons in plaats daarvan zouden richten op de verschillende toestanden van de applicatie. Hoeveel toestanden hebben we, en wat zijn hun mogelijke ingangen? We nemen hetzelfde voorbeeld:
- idle
In deze toestand geven we de knop fetch-data weer, gaan zitten en wachten. De mogelijke acties zijn:- klik
Als de gebruiker op de knop klikt, vuren we het verzoek naar de backend en brengen we de machine in een “fetching”-toestand.
- klik
- fetching
Het verzoek is in de lucht, en we zitten en wachten. De acties zijn:- succes
De gegevens komen met succes aan en zijn niet beschadigd. We gebruiken de gegevens op een of andere manier en gaan terug naar de “idle” state. - failure
Als er een fout optreedt bij het doen van het verzoek of het parsen van de gegevens, gaan we over naar een “error” state.
- succes
- retry
Als de gebruiker op de knop retry klikt, wordt het verzoek opnieuw uitgevoerd en gaat de machine over naar de status “fetching”.
error
We tonen een foutmelding en tonen de fetch-data knop. Deze status accepteert één actie:
We hebben ruwweg dezelfde processen beschreven, maar dan met toestanden en ingangen.
Dit vereenvoudigt de logica en maakt het voorspelbaarder. Het lost ook enkele van de hierboven genoemde problemen op. Merk op dat, terwijl we in “fetching” status zijn, we geen kliks accepteren. Dus, zelfs als de gebruiker op de knop klikt, zal er niets gebeuren omdat de machine niet geconfigureerd is om op die actie te reageren terwijl hij in die toestand is. Deze aanpak elimineert automatisch de onvoorspelbare vertakking van onze code logica. Dit betekent dat we tijdens het testen minder code hoeven te behandelen. Bovendien kunnen sommige types van testen, zoals integratietesten, geautomatiseerd worden. Bedenk hoe we een duidelijk idee zouden hebben van wat onze applicatie doet, en we zouden een script kunnen maken dat de gedefinieerde toestanden en overgangen doorloopt en dat asserties genereert. Deze asserties zouden kunnen bewijzen dat we alle mogelijke toestanden hebben bereikt of een bepaald traject hebben afgelegd.
In feite is het opschrijven van alle mogelijke toestanden eenvoudiger dan het opschrijven van alle mogelijke overgangen, omdat we weten welke toestanden we nodig hebben of hebben. Trouwens, in de meeste gevallen zouden de toestanden de bedrijfslogica van onze applicatie beschrijven, terwijl de overgangen in het begin heel vaak onbekend zijn. De bugs in onze software zijn het gevolg van acties die in een verkeerde toestand en/of op het verkeerde moment worden uitgevoerd. Ze laten onze applicatie achter in een toestand die we niet kennen, en dit breekt ons programma of laat het zich verkeerd gedragen. Natuurlijk willen we niet in zo’n situatie terechtkomen. State machines zijn goede firewalls. Ze beschermen ons tegen het bereiken van onbekende toestanden omdat we grenzen stellen aan wat kan gebeuren en wanneer, zonder expliciet te zeggen hoe. Het concept van een toestandsmachine gaat heel goed samen met een unidirectionele gegevensstroom. Samen verminderen ze de complexiteit van de code en ontrafelen ze het mysterie waar een toestand vandaan komt.
Een toestandsmachine maken in JavaScript
Goeg gepraat – laten we eens wat code bekijken. We zullen hetzelfde voorbeeld gebruiken. Op basis van de lijst hierboven beginnen we met het volgende:
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } }}
We hebben de toestanden als objecten en hun mogelijke ingangen als functies. De begintoestand ontbreekt echter. Laten we de bovenstaande code in het volgende veranderen:
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } }}
Als we alle toestanden hebben gedefinieerd die voor ons zinvol zijn, zijn we klaar om de invoer te versturen en van toestand te veranderen. Dat doen we met behulp van de twee onderstaande helper methods:
const machine = { dispatch(actionName, ...payload) { const actions = this.transitions; const action = this.transitions; if (action) { action.apply(machine, ...payload); } }, changeStateTo(newState) { this.state = newState; }, ...}
De dispatch
functie controleert of er een actie met de gegeven naam in de overgangen van de huidige status zit. Zo ja, dan vuurt hij die af met de gegeven payload. We roepen ook de action
handler aan met de machine
als context, zodat we andere acties kunnen dispatchen met this.dispatch(<action>)
of de status kunnen veranderen met this.changeStateTo(<new state>)
.
Als we de gebruikersreis van ons voorbeeld volgen, is de eerste actie die we moeten verzenden click
. Hier is hoe de handler van die actie eruit ziet:
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');
We veranderen eerst de status van de machine in fetching
. Dan, starten we het verzoek naar de back end. Laten we aannemen dat we een service hebben met een methode getData
die een belofte teruggeeft. Zodra deze is opgelost en de data parsing OK is, dispatchen we success
, zo niet failure
.
Zo ver, zo goed. Nu moeten we success
en failure
acties en ingangen onder de fetching
state implementeren:
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ...}
Merk op hoe we onze hersenen hebben bevrijd van de noodzaak om na te denken over het vorige proces. We geven niet om gebruikersklikken of wat er gebeurt met het HTTP-verzoek. We weten dat de applicatie in een fetching
staat is, en we verwachten alleen deze twee acties. Het is een beetje als het schrijven van nieuwe logica in isolatie.
Het laatste stukje is de error
state. Het zou mooi zijn als we die retry-logica zouden verschaffen, zodat de applicatie kan herstellen van een mislukking.
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } }}
Hier moeten we de logica dupliceren die we in de click
handler hebben geschreven. Om dat te voorkomen, moeten we of de handler definiëren als een functie die voor beide acties toegankelijk is, of we gaan eerst naar de idle
status en sturen dan de click
actie handmatig.
Een volledig voorbeeld van de werkende state machine is te vinden in mijn Codepen.
Managing State Machines With A Library
Het finite state machine patroon werkt ongeacht of we React, Vue of Angular gebruiken. Zoals we in de vorige sectie hebben gezien, kunnen we zonder veel moeite een state machine implementeren. Soms biedt een library echter meer flexibiliteit. Enkele van de goede zijn Machina.js en XState. In dit artikel zullen we het echter hebben over Stent, mijn Redux-achtige bibliotheek die het concept van eindige toestandsmachines inbouwt.
Stent is een implementatie van een toestandsmachines container. Het volgt een aantal van de ideeën in de Redux en Redux-Saga projecten, maar biedt, naar mijn mening, eenvoudigere en boilerplate-vrije processen. Het is ontwikkeld met behulp van readme-driven development, en ik heb letterlijk weken besteed aan alleen het API ontwerp. Omdat ik de bibliotheek aan het schrijven was, had ik de kans om de problemen op te lossen die ik tegenkwam bij het gebruik van de Redux en Flux architecturen.
Creating Machines
In de meeste gevallen bestrijken onze applicaties meerdere domeinen. We kunnen niet volstaan met slechts één machine. Daarom maakt Stent het mogelijk om veel machines te maken:
import { Machine } from 'stent';const machineA = Machine.create('A', { state: ..., transitions: ...});const machineB = Machine.create('B', { state: ..., transitions: ...});
Later kunnen we toegang krijgen tot deze machines met behulp van de Machine.get
methode:
const machineA = Machine.get('A');const machineB = Machine.get('B');
Connecting The Machines To The Rendering Logic
Rendering wordt in mijn geval gedaan via React, maar we kunnen elke andere library gebruiken. Het komt neer op het afvuren van een callback waarin we de rendering triggeren. Een van de eerste functies waar ik aan werkte was de connect
functie:
import { connect } from 'stent/lib/helpers';Machine.create('MachineA', ...);Machine.create('MachineB', ...);connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });
We zeggen welke machines voor ons belangrijk zijn en geven hun namen op. De callback die we doorgeven aan map
wordt in eerste instantie een keer afgevuurd en later telkens als de status van een van de machines verandert. Dit is waar we de rendering triggeren. Op dit punt hebben we direct toegang tot de aangesloten machines, dus we kunnen de huidige status en methodes ophalen. Er zijn ook mapOnce
, om de callback maar één keer afgevuurd te krijgen, en mapSilent
, om die initiële uitvoering over te slaan.
Voor het gemak is er een helper geëxporteerd speciaal voor React integratie. Het is echt vergelijkbaar met Redux’s 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 functionexport default connect(TodoList) .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { isIdle: MachineA.isIdle, todos: MachineB.state.todos });
Stent voert onze mapping callback uit en verwacht een object te ontvangen – een object dat als props
naar onze React component wordt gestuurd.
Wat is state in de context van Stent?
Tot nu toe bestond onze state uit eenvoudige strings. Helaas, in de echte wereld, moeten we meer dan een string in state houden. Daarom is de status van Stent eigenlijk een object met eigenschappen erin. De enige gereserveerde eigenschap is name
. Al het andere is app-specifieke data. Bijvoorbeeld:
{ name: 'idle' }{ name: 'fetching', todos: }{ name: 'forward', speed: 120, gear: 4 }
Mijn ervaring met Stent tot nu toe leert me dat als het state object groter wordt, we waarschijnlijk een andere machine nodig hebben die deze extra eigenschappen afhandelt. Het identificeren van de verschillende toestanden kost wat tijd, maar ik denk dat dit een grote stap voorwaarts is in het schrijven van beter beheersbare applicaties. Het is een beetje als het voorspellen van de toekomst en het tekenen van kaders van de mogelijke acties.
Werken met de toestandsmachine
Gelijk aan het voorbeeld in het begin, moeten we de mogelijke (eindige) toestanden van onze machine definiëren en de mogelijke ingangen beschrijven:
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' }; } } }});
We hebben onze begintoestand, idle
, die een actie accepteert van run
. Zodra de machine in een running
staat is, zijn we in staat om de stop
actie af te vuren, die ons terug brengt naar de idle
staat.
U herinnert zich waarschijnlijk nog wel de dispatch
en changeStateTo
helpers van onze eerdere implementatie. Deze bibliotheek biedt dezelfde logica, maar het is intern verborgen, en we hoeven er niet over na te denken. Voor het gemak, gebaseerd op de transitions
eigenschap, genereert Stent het volgende:
- helper methods om te controleren of de machine zich in een bepaalde toestand bevindt – de
idle
toestand levert deisIdle()
methode op, terwijl we voorrunning
isRunning()
hebben; - hulpmethodes voor het dispatchen van acties:
runPlease()
enstopNow()
.
In het bovenstaande voorbeeld kunnen we dus het volgende gebruiken:
machine.isIdle(); // booleanmachine.isRunning(); // booleanmachine.runPlease(); // fires actionmachine.stopNow(); // fires action
Door de automatisch gegenereerde methoden te combineren met de connect
utiliteitsfunctie, zijn we in staat de cirkel te sluiten. Een gebruikersinteractie triggert de machine-input en -actie, die de toestand bijwerkt. Door die update wordt de mapping-functie die aan connect
is doorgegeven, afgevuurd, en worden wij op de hoogte gebracht van de toestandsverandering. Daarna renderen we.
Invoer- en actiehandlers
Het belangrijkste onderdeel zijn waarschijnlijk de actiehandlers. Dit is de plaats waar we het grootste deel van de applicatie logica schrijven omdat we reageren op input en veranderde toestanden. Iets wat ik erg leuk vind in Redux is hier ook geïntegreerd: de onveranderlijkheid en eenvoud van de reducer functie. De essentie van Stent’s action handler is hetzelfde. Het ontvangt de huidige status en actie payload, en het moet de nieuwe status teruggeven. Als de handler niets teruggeeft (undefined
), dan blijft de toestand van de machine hetzelfde.
transitions: { 'fetching': { 'success': function (state, payload) { const todos = ; return { name: 'idle', todos }; } }}
Laten we aannemen dat we data moeten ophalen van een server op afstand. We voeren het verzoek uit en zetten de machine in een fetching
status. Zodra de gegevens van de back-end komen, vuren we een success
actie af, zoals dit:
machine.success({ label: '...' });
Dan gaan we terug naar een idle
toestand en bewaren wat gegevens in de vorm van de todos
array. Er zijn nog een paar andere mogelijke waarden om in te stellen als action handlers. Het eerste en eenvoudigste geval is wanneer we alleen een string doorgeven die de nieuwe status wordt.
transitions: { 'idle': { 'run': 'running' }}
Dit is een overgang van { name: 'idle' }
naar { name: 'running' }
met behulp van de run()
actie. Deze aanpak is nuttig wanneer we synchrone toestandsovergangen hebben en geen meta-data. Dus, als we iets anders in state houden, zal dat type van overgang het wegspoelen. Op dezelfde manier kunnen we een state object direct doorgeven:
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: } }}
We maken een overgang van editing
naar idle
met behulp van de deleteAllTodos
actie.
We hebben de functie handler al gezien, en de laatste variant van de actie handler is een generator functie. Het is geïnspireerd door het Redux-Saga project, en het ziet er als volgt uit:
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 }; } } }});
Als je geen ervaring hebt met generators, ziet dit er misschien een beetje cryptisch uit. Maar de generatoren in JavaScript zijn een krachtig hulpmiddel. We mogen onze action handler pauzeren, meerdere keren van status veranderen en async-logica afhandelen.
Leuk met generatoren
Toen ik voor het eerst kennismaakte met Redux-Saga, dacht ik dat het een overgecompliceerde manier was om async-operaties af te handelen. In feite is het een behoorlijk slimme implementatie van het commando ontwerp patroon. Het belangrijkste voordeel van dit patroon is dat het de aanroeping van logica en de feitelijke implementatie ervan scheidt.
Met andere woorden, we zeggen wat we willen, maar niet hoe het moet gebeuren. Matt Hink’s blog serie heeft me geholpen te begrijpen hoe saga’s worden geïmplementeerd, en ik raad sterk aan het te lezen. Ik heb dezelfde ideeën meegenomen naar Stent, en voor het doel van dit artikel, zullen we zeggen dat door dingen te geven, we instructies geven over wat we willen zonder het daadwerkelijk te doen. Zodra de actie is uitgevoerd, krijgen we de controle terug.
Op dit moment kunnen een paar dingen worden uitgezonden (yielded):
- een state object (of een string) voor het veranderen van de toestand van de machine;
- een aanroep van de
call
helper (het accepteert een synchrone functie, dat is een functie die een belofte of een andere generator functie retourneert) – we zeggen in feite: “Voer dit voor mij uit, en als het asynchroon is, wacht. Als je klaar bent, geef me dan het resultaat.”; - een oproep van de
wait
helper (deze accepteert een string die een andere actie vertegenwoordigt); als we deze utiliteitsfunctie gebruiken, pauzeren we de handler en wachten we op een andere actie die wordt verzonden.
Hier volgt een functie die de varianten illustreert:
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 = 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 }; } }}
Zoals we kunnen zien, ziet de code er synchroon uit, maar dat is het in feite niet. Het is gewoon Stent die het saaie deel doet van het wachten op de opgeloste belofte of het itereren over een andere generator.
Hoe Stent mijn Redux-zorgen oplost
Te veel code
De Redux- (en Flux-)architectuur vertrouwt op acties die in ons systeem circuleren. Als de applicatie groeit, hebben we meestal een heleboel constanten en actie makers. Deze twee dingen zitten vaak in verschillende mappen, en het bijhouden van de uitvoering van de code kost soms tijd. Ook moeten we bij het toevoegen van een nieuwe functie altijd een hele set acties afhandelen, wat betekent dat we meer actienamen en actiecreators moeten definiëren.
In Stent hebben we geen actienamen, en de bibliotheek maakt de actiecreators automatisch voor ons aan:
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } }});machine.addTodo({ title: 'Fix that bug' });
We hebben de machine.addTodo
actiecreator direct gedefinieerd als een methode van de machine. Deze aanpak loste ook een ander probleem op waar ik tegenaan liep: het vinden van de reducer die reageert op een bepaalde actie. Gewoonlijk zien we in React-componenten action creator-namen zoals addTodo
; in de reducers werken we echter met een type actie dat constant is. Soms moet ik naar de code van de action creator springen om het exacte type te kunnen zien. Hier hebben we helemaal geen types.
Onvoorspelbare State Changes
In het algemeen doet Redux goed werk met het beheren van state op een immutable manier. Het probleem zit hem niet in Redux zelf, maar in het feit dat de ontwikkelaar elke actie op elk moment mag dispatchen. Als we zeggen dat we een actie hebben die de lichten aan doet, is het dan OK om die actie twee keer achter elkaar af te vuren? Zo niet, hoe moeten we dit probleem dan oplossen met Redux? Nou, we zouden waarschijnlijk wat code in de reducer stoppen die de logica beschermt en die controleert of de lichten al aan zijn – misschien een if
clausule die de huidige status controleert. Nu is de vraag, valt dit niet buiten het bereik van de reducer?
Wat ik mis in Redux is een manier om het dispatchen van een actie op basis van de huidige status van de applicatie te stoppen zonder de reducer te vervuilen met conditionele logica. En ik wil deze beslissing ook niet meenemen naar de view layer, waar de action creator wordt afgevuurd. Met Stent gebeurt dit automatisch omdat de machine niet reageert op acties die niet in de huidige status worden gedeclareerd. Bijvoorbeeld:
const machine = Machine.create('app', { state: { name: 'idle' }, transitions: { 'idle': { 'run': 'running', 'jump': 'jumping' }, 'running': { 'stop': 'idle' } }});// this is finemachine.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();
Het feit dat de machine alleen specifieke inputs accepteert op een bepaald moment beschermt ons tegen rare bugs en maakt onze applicaties voorspelbaarder.
States, Not Transitions
Redux, net als Flux, laat ons denken in termen van overgangen. Het mentale model van ontwikkelen met Redux wordt vooral gedreven door acties en hoe deze acties de toestand in onze reducers transformeren. Dat is niet slecht, maar ik heb ontdekt dat het zinvoller is om in plaats daarvan in termen van toestanden te denken – in welke toestanden de app zich zou kunnen bevinden en hoe deze toestanden de zakelijke vereisten vertegenwoordigen.
Conclusie
Het concept van toestandsmachines in programmeren, vooral in UI-ontwikkeling, was oog-openend voor me. Ik begon overal toestandsmachines te zien, en ik heb een soort verlangen om altijd naar dat paradigma over te stappen. Ik zie zeker de voordelen van het hebben van meer strikt gedefinieerde toestanden en overgangen tussen hen. Ik ben altijd op zoek naar manieren om mijn apps eenvoudig en leesbaar te maken. Ik geloof dat toestandsmachines een stap in die richting zijn. Het concept is eenvoudig en tegelijkertijd krachtig. Het heeft de potentie om een hoop bugs te elimineren.