Benutzeroberflächen können durch zwei Dinge ausgedrückt werden
- Der Zustand der Benutzeroberfläche
- Aktionen, die diesen Zustand ändern können
Von Kreditkartenlesegeräten und Bildschirmen von Zapfsäulen bis hin zur Software, die Ihr Unternehmen entwickelt, reagieren Benutzeroberflächen auf die Aktionen des Benutzers und andere Quellen und ändern ihren Zustand entsprechend. Dieses Konzept ist nicht nur auf die Technologie beschränkt, es ist ein grundlegender Bestandteil, wie *alles* funktioniert
Zu jeder Aktion gibt es eine gleichwertige und entgegengesetzte Reaktion.
– Isaac Newton
Dies ist ein Konzept, das wir auf die Entwicklung besserer Benutzeroberflächen anwenden können, aber bevor wir dorthin gelangen, möchte ich, dass Sie etwas ausprobieren. Betrachten Sie eine Fotogalerie-Oberfläche mit diesem Benutzerinteraktionsfluss
- Zeigen Sie ein Suchfeld und eine Suchschaltfläche an, mit denen der Benutzer nach Fotos suchen kann
- Wenn die Suchschaltfläche angeklickt wird, rufen Sie Fotos mit dem Suchbegriff von Flickr ab
- Zeigen Sie die Suchergebnisse in einem Raster aus kleinen Fotos an
- Wenn ein Foto angeklickt/getippt wird, zeigen Sie das Foto in voller Größe an
- Wenn ein Vollbildfoto erneut angeklickt/getippt wird, kehren Sie zur Galerieansicht zurück
Überlegen Sie nun, wie Sie es entwickeln würden. Versuchen Sie vielleicht sogar, es in React zu programmieren. Ich warte; ich bin nur ein Artikel. Ich gehe nirgendwohin.
Fertig? Großartig! Das war nicht zu schwierig, oder? Denken Sie nun über die folgenden Szenarien nach, die Sie vielleicht vergessen haben
- Was passiert, wenn der Benutzer wiederholt auf die Suchschaltfläche klickt?
- Was passiert, wenn der Benutzer die Suche abbrechen möchte, während sie noch läuft?
- Ist die Suchschaltfläche während der Suche deaktiviert?
- Was passiert, wenn der Benutzer die deaktivierte Schaltfläche bösartig aktiviert?
- Gibt es eine Anzeige dafür, dass die Ergebnisse geladen werden?
- Was passiert, wenn ein Fehler auftritt? Kann der Benutzer die Suche wiederholen?
- Was passiert, wenn der Benutzer sucht und dann auf ein Foto klickt? Was sollte passieren?
Dies sind nur *einige* der potenziellen Probleme, die während der Planung, Entwicklung oder beim Testen auftreten können. Wenig ist schlimmer in der Softwareentwicklung, als zu glauben, alle möglichen Anwendungsfälle abgedeckt zu haben, und dann neue Edge Cases zu entdecken (oder zu erhalten), die Ihren Code weiter verkomplizieren, sobald Sie sie berücksichtigen. Es ist besonders schwierig, in ein bestehendes Projekt einzusteigen, bei dem all diese Anwendungsfälle undokumentiert sind, sondern stattdessen in Spaghetti-Code versteckt sind und darauf warten, dass Sie ihn entschlüsseln.
Das Offensichtliche darlegen
Was wäre, wenn wir alle möglichen UI-Zustände ermitteln könnten, die aus allen möglichen Aktionen, die auf jeden Zustand angewendet werden, resultieren? Und was wäre, wenn wir diese Zustände, Aktionen und Übergänge zwischen Zuständen visualisieren könnten? Designer tun dies intuitiv, in sogenannten „User Flows“ (oder „UX Flows“), um darzustellen, wie der nächste Zustand der UI in Abhängigkeit von der Benutzerinteraktion aussehen soll.

In der Informatik gibt es ein Berechnungsmodell namens endlicher Automat, oder „endliche Zustandsautomaten“ (FSM), das die gleiche Art von Information ausdrücken kann. Das heißt, sie beschreiben, welcher Zustand als nächstes eintritt, wenn eine Aktion auf den aktuellen Zustand angewendet wird. Genau wie User Flows können diese endlichen Zustandsautomaten klar und eindeutig visualisiert werden. Hier ist zum Beispiel das Zustandsübergangsdiagramm, das den FSM einer Ampel beschreibt

Was ist ein endlicher Zustandsautomat?
Ein Zustandsautomat ist eine nützliche Methode zur Modellierung des Verhaltens in einer Anwendung: Für jede Aktion gibt es eine Reaktion in Form einer Zustandsänderung. Eine klassische endliche Zustandsmaschine hat 5 Teile
- Eine Menge von Zuständen (z. B.
idle,loading,success,errorusw.) - Eine Menge von Aktionen (z. B.
SEARCH,CANCEL,SELECT_PHOTOusw.) - Ein Anfangszustand (z. B.
idle) - Eine Übergangsfunktion (z. B.
transition('idle', 'SEARCH') == 'loading') - Endzustände (die für diesen Artikel nicht relevant sind.)
Deterministische endliche Zustandsautomaten (mit denen wir uns befassen werden) haben ebenfalls einige Einschränkungen
- Es gibt eine endliche Anzahl möglicher Zustände
- Es gibt eine endliche Anzahl möglicher Aktionen (das sind die „endlichen“ Teile)
- Die Anwendung kann sich zu jedem Zeitpunkt nur in einem dieser Zustände befinden
- Gegeben ein
currentStateund eineaction, muss die Übergangsfunktion immer denselbennextStatezurückgeben (das ist der „deterministische“ Teil)
Darstellung von endlichen Zustandsautomaten
Ein endlicher Zustandsautomat kann als eine Zuordnung von einem state zu seinen „Übergängen“ dargestellt werden, wobei jeder Übergang eine action und der nextState ist, der auf diese Aktion folgt. Diese Zuordnung ist ein einfaches JavaScript-Objekt.
Betrachten wir ein Beispiel einer amerikanischen Ampel, eines der einfachsten FSM-Beispiele. Nehmen wir an, wir starten auf green, wechseln dann nach einiger TIMER zu yellow und dann nach einem weiteren TIMER zu RED und nach einem weiteren TIMER zurück zu green
const machine = {
green: { TIMER: 'yellow' },
yellow: { TIMER: 'red' },
red: { TIMER: 'green' }
};
const initialState = 'green';
Eine Übergangsfunktion beantwortet die Frage
Was ist der nächste Zustand, wenn der aktuelle Zustand und eine Aktion gegeben sind?
Bei unserem Setup ist der Übergang zum nächsten Zustand basierend auf einer Aktion (in diesem Fall TIMER) nur ein Nachschlagen des currentState und der action im machine-Objekt, da
machine[currentState]gibt uns die nächste Aktionszuordnung, z. B.:machine['green'] == {TIMER: 'yellow'}machine[currentState][action]gibt uns den nächsten Zustand aus der Aktion, z. B.:machine['green']['TIMER'] == 'yellow'
// ...
function transition(currentState, action) {
return machine[currentState][action];
}
transition('green', 'TIMER');
// => 'yellow'
Anstatt if/else- oder switch-Anweisungen zu verwenden, um den nächsten Zustand zu bestimmen, z. B. if (currentState === 'green') return 'yellow';, haben wir die gesamte Logik in ein einfaches JavaScript-Objekt verschoben, das in JSON serialisiert werden kann. Das ist eine Strategie, die sich in Bezug auf Tests, Visualisierung, Wiederverwendung, Analyse, Flexibilität und Konfigurierbarkeit stark auszahlen wird.
Siehe den Pen Einfaches Beispiel für einen endlichen Zustandsautomaten von David Khourshid (@davidkpiano) auf CodePen.
Endliche Zustandsautomaten in React
Betrachten wir ein komplexeres Beispiel, wie wir unsere Galerie-App mit einem endlichen Zustandsautomaten darstellen können. Die App kann sich in einem von mehreren Zuständen befinden
start– die Ansicht der anfänglichen Suchseiteloading– Ansicht zum Abrufen von Suchergebnissenerror– Ansicht, wenn die Suche fehlschlägtgallery– Ansicht der erfolgreichen Suchergebnissephoto– Ansicht eines einzelnen detaillierten Fotos
Und mehrere Aktionen können ausgeführt werden, entweder vom Benutzer oder von der App selbst
SEARCH– der Benutzer klickt auf die Schaltfläche „Suchen“SEARCH_SUCCESS– die Suche war erfolgreich mit den abgefragten FotosSEARCH_FAILURE– die Suche ist aufgrund eines Fehlers fehlgeschlagenCANCEL_SEARCH– der Benutzer klickt auf die Schaltfläche „Suche abbrechen“SELECT_PHOTO– der Benutzer klickt auf ein Foto in der GalerieEXIT_PHOTO– der Benutzer klickt, um die detaillierte Fotoansicht zu beenden
Der beste Weg, um zunächst zu visualisieren, wie diese Zustände und Aktionen zusammenkommen, sind zwei sehr mächtige Werkzeuge: Bleistift und Papier. Zeichnen Sie Pfeile zwischen den Zuständen und beschriften Sie die Pfeile mit den Aktionen, die Zustandsübergänge zwischen den Zuständen verursachen
Wir können diese Übergänge nun in einem Objekt darstellen, genau wie im Beispiel der Ampel
const galleryMachine = {
start: {
SEARCH: 'loading'
},
loading: {
SEARCH_SUCCESS: 'gallery',
SEARCH_FAILURE: 'error',
CANCEL_SEARCH: 'gallery'
},
error: {
SEARCH: 'loading'
},
gallery: {
SEARCH: 'loading',
SELECT_PHOTO: 'photo'
},
photo: {
EXIT_PHOTO: 'gallery'
}
};
const initialState = 'start';
Nun sehen wir, wie wir diese Konfiguration des endlichen Zustandsautomaten und die Übergangsfunktion in unsere Galerie-App integrieren können. Im Zustand der App-Komponente wird es eine einzelne Eigenschaft geben, die den aktuellen endlichen Zustand angibt: gallery
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
gallery: 'start', // initial finite state
query: '',
items: []
};
}
// ...
Die transition-Funktion wird eine Methode dieser App-Klasse sein, damit wir den aktuellen endlichen Zustand abrufen können
// ...
transition(action) {
const currentGalleryState = this.state.gallery;
const nextGalleryState =
galleryMachine[currentGalleryState][action.type];
if (nextGalleryState) {
const nextState = this.command(nextGalleryState, action);
this.setState({
gallery: nextGalleryState,
...nextState // extended state
});
}
}
// ...
Dies ähnelt der zuvor beschriebenen Funktion transition(currentState, action), mit einigen Unterschieden
- Die
actionist ein Objekt mit einer Eigenschafttype, die den String-Aktionstyp angibt, z. B.type: 'SEARCH' - Nur die
actionwird übergeben, da wir den aktuellen endlichen Zustand austhis.state.galleryabrufen können - Der gesamte App-Zustand wird mit dem nächsten endlichen Zustand, d. h.
nextGalleryState, sowie mit jedem *erweiterten Zustand* (nextState) aktualisiert, der sich aus der Ausführung eines Befehls basierend auf der Nutzlast des nächsten Zustands und der Aktion ergibt (siehe Abschnitt „Befehle ausführen“)
Befehle ausführen
Wenn eine Zustandsänderung eintritt, können „Seiteneffekte“ (oder „Befehle“, wie wir sie nennen werden) ausgeführt werden. Wenn ein Benutzer beispielsweise auf die Schaltfläche „Suchen“ klickt und eine 'SEARCH'-Aktion ausgelöst wird, wechselt der Zustand zu 'loading', und eine asynchrone Flickr-Suche sollte ausgeführt werden (andernfalls wäre 'loading' eine Lüge, und Entwickler sollten niemals lügen).
Wir können diese Seiteneffekte in einer command(nextState, action)-Methode behandeln, die bestimmt, was ausgeführt werden soll, basierend auf dem nächsten endlichen Zustand und der Aktionsnutzlast, sowie wie der *erweiterte Zustand* aussehen soll
// ...
command(nextState, action) {
switch (nextState) {
case 'loading':
// execute the search command
this.search(action.query);
break;
case 'gallery':
if (action.items) {
// update the state with the found items
return { items: action.items };
}
break;
case 'photo':
if (action.item) {
// update the state with the selected photo item
return { photo: action.item };
}
break;
default:
break;
}
}
// ...
Aktionen können Nutzlasten enthalten, die über den type der Aktion hinausgehen und mit denen der App-Zustand aktualisiert werden muss. Wenn beispielsweise eine Aktion 'SEARCH' erfolgreich ist, kann eine Aktion 'SEARCH_SUCCESS' mit den items aus dem Suchergebnis ausgelöst werden
// ...
fetchJsonp(
`https://api.flickr.com/services/feeds/photos_public.gne?lang=en-us&format=json&tags=${encodedQuery}`,
{ jsonpCallback: 'jsoncallback' })
.then(res => res.json())
.then(data => {
this.transition({ type: 'SEARCH_SUCCESS', items: data.items });
})
.catch(error => {
this.transition({ type: 'SEARCH_FAILURE' });
});
// ...
Die obige command()-Methode gibt sofort jeden erweiterten Zustand (d. h. Zustand, der vom endlichen Zustand abweicht) zurück, mit dem this.state in this.setState(...) aktualisiert werden soll, zusammen mit der endlichen Zustandsänderung.
Die finale maschinenkontrollierte App
Da wir den endlichen Zustandsautomaten für die App deklarativ konfiguriert haben, können wir die richtige Benutzeroberfläche auf sauberere Weise rendern, indem wir bedingt basierend auf dem aktuellen endlichen Zustand rendern
// ...
render() {
const galleryState = this.state.gallery;
return (
<div className="ui-app" data-state={galleryState}>
{this.renderForm(galleryState)}
{this.renderGallery(galleryState)}
{this.renderPhoto(galleryState)}
</div>
);
}
// ...
Das Endergebnis
Siehe den Pen Galerie-App mit endlichen Zustandsautomaten von David Khourshid (@davidkpiano) auf CodePen.
Zustand in CSS
Sie haben möglicherweise data-state={galleryState} im obigen Code bemerkt. Durch Setzen dieses Datenattributs können wir jeden Teil unserer App mithilfe eines Attributselektors bedingt stylen
.ui-app {
// ...
&[data-state="start"] {
justify-content: center;
}
&[data-state="loading"] {
.ui-item {
opacity: .5;
}
}
}
Dies ist besser als die Verwendung von className, da Sie die Einschränkung durchsetzen können, dass nur ein Wert gleichzeitig für data-state gesetzt werden kann, und die Spezifität die gleiche ist wie bei der Verwendung einer Klasse. Attributselektoren werden auch in den meisten gängigen CSS-in-JS-Lösungen unterstützt.
Vorteile und Ressourcen
Die Verwendung von endlichen Zustandsautomaten zur Beschreibung des Verhaltens komplexer Anwendungen ist nichts Neues. Traditionell geschah dies mit switch- und goto-Anweisungen, aber durch die Beschreibung endlicher Zustandsautomaten als eine deklarative Zuordnung zwischen Zuständen, Aktionen und nächsten Zuständen können Sie diese Daten zur Visualisierung von Zustandsübergängen verwenden

Darüber hinaus ermöglicht die Verwendung deklarativer endlicher Zustandsautomaten Ihnen
- Anwendungslogik überall speichern, teilen und konfigurieren – ähnliche Komponenten, andere Apps, in Datenbanken, in anderen Sprachen usw.
- Die Zusammenarbeit mit Designern und Projektmanagern erleichtern
- Zustandsübergänge statisch analysieren und optimieren, einschließlich Zuständen, die unmöglich zu erreichen sind
- Anwendungslogik ohne Angst leicht ändern
- Integrationstests automatisieren
Fazit und Erkenntnisse
Endliche Zustandsautomaten sind eine *Abstraktion* zur Modellierung der Teile Ihrer App, die als endliche Zustände dargestellt werden können, und fast alle Apps haben diese Teile. Die in diesem Artikel vorgestellten FSM-Codierungsmuster
- Können mit jedem bestehenden State-Management-Setup verwendet werden; z. B. Redux oder MobX
- Können an jedes Framework (nicht nur React) oder gar kein Framework angepasst werden
- Sind nicht in Stein gemeißelt; der Entwickler kann die Muster an seinen Programmierstil anpassen
- Sind nicht für jede Situation oder jeden Anwendungsfall anwendbar
Ich ermutige Sie von nun an, wenn Sie auf „Boolean Flag“-Variablen wie isLoaded oder isSuccess stoßen, innezuhalten und darüber nachzudenken, wie Ihr App-Zustand stattdessen als endlicher Zustandsautomat modelliert werden kann. Auf diese Weise können Sie Ihre App refaktorisieren, um den Zustand als state === 'loaded' oder state === 'success' darzustellen, wobei aufzählbare Zustände anstelle von booleschen Flags verwendet werden.
Ressourcen
Ich habe auf der React Rally 2017 einen Vortrag über die Verwendung von endlichen Automaten und Statecharts zur Erstellung besserer Benutzeroberflächen gehalten, wenn Sie mehr über die Motivation und Prinzipien erfahren möchten
Folien: Unendlich bessere Benutzeroberflächen mit endlichen Automaten
Hier sind einige weitere Ressourcen
- Pure UI von Guillermo Rauch
- Pure UI Control von Adam Solove
- Wikipedia: Finite State Machines
- Statecharts: A Visual Formalism for Complex Systems von David Harel (PDF)
- Managing State in JavaScript with State Machines von Krasimir Tsonev
- Rambling Thoughts on React and Finite State Machines von Ryan Florence
Toller Artikel. Aber wird der durchschnittliche imperative Programmierer je das Licht sehen? Wenn sie Ihre exzellente Arbeit lesen, würden sie es wahrscheinlich tun und dann dafür kritisiert werden, dass sie zu lange brauchen. So ist das Leben
Danke! Ich würde argumentieren, dass der obige Code sogar kürzer und prägnanter ist als typischer imperativer Code. Und Sie verbringen weniger Zeit mit dem Codieren, weil Sie sich keine Gedanken über das Erreichen unmöglichen Zuständen machen müssen.
Ich habe mit meinem letzten Projekt stately.js (https://github.com/fschaefer/Stately.js) verwendet, das mir viel Arbeit abgenommen hat.
Großartiger Artikel und die CSS-Ideen gefallen mir wirklich. Ich habe festgestellt, dass die Nutzung der Elm-Union-Typen zur Erstellung von FSM eine intuitive Art geworden ist, CSS-Animationen zu verwenden. Ich hätte nie daran gedacht, es in eine reine JS-Umgebung zu bringen. Es lässt mich wieder mit React anfangen. Interessante Idee, Datenattribute zu verwenden.
Ich habe tatsächlich Dinge in JS mit einer selbst erstellten Bibliothek gebaut, die ähnliche Konzepte aus dem Studium endlicher Automaten verwendet. Ich fand, dass es die Dinge zwar vielleicht klarer machte, aber stark von dem abhing, was ich baute, und bei größeren UI-Implementierungen etwas überwältigend wirken konnte.
Die Vorteile für mich waren, dass es super einfach war, die Zustände/Übergänge von Anwendungen abzubilden, und ich fand, dass das ein echter Augenöffner in Bezug auf das Anwendungszustandsmanagement war.
Aber ein ausgezeichneter Artikel! Ich denke, es ist definitiv etwas, das mehr Web-Entwickler in Betracht ziehen sollten, und wenn sie so sind wie ich, werden sie das wahrscheinlich viel annehmbarer finden als die üblichen obszön glitzernden und masturbatorischen Erklärungen von FSA, die das Internet und Lehrbücher plagen.
FSMs (und Statecharts) sollten niemals vollständig von der gebauten Benutzeroberfläche abhängen. Die Benutzeroberfläche sollte nur eine Schicht sein, mit der der Benutzer interagieren kann, die Aktionen auslösen kann, und der FSM sollte nur dafür verantwortlich sein, den nächsten Zustand basierend auf diesen Aktionen und dem aktuellen Zustand zu bestimmen.
Für größere UI-Implementierungen empfehle ich dringend, sich über Statecharts zu informieren, die eine Erweiterung endlicher Zustandsautomaten sind und viele der Skalierungsprobleme lösen, auf die Sie bei FSMs und komplexen UIs mit mehr Zuständen stoßen werden.
Ich werde bald einen Artikel über Statecharts schreiben!
Anstatt die „loading“-Eigenschaft bei SUCCESS oder sogar bei ERROR zu ändern, können Sie ein weiteres .then() nach .catch() verwenden und die „loading“-Eigenschaft auf false setzen. Das ist einfach SoC und DRY.
Können Sie das klarstellen? Es gibt keine „loading“-Eigenschaft; das ist der Sinn der Sache. Sie sollten die endlichen Zustände aufzählen, anstatt eine Sammlung von booleschen Flags in Ihrer App zu haben.
Wenn Sie boolesche Flags wie „loading“ haben, haben Sie technisch gesehen 2^n mögliche Zustände (wobei „n“ die Anzahl der booleschen Flags ist). Dann liegt es an Ihnen, dem Entwickler, sicherzustellen, dass eine große Teilmenge dieser 2^n Zustände unmöglich ist, indem Sie alberne Dinge tun wie „if (!loading && success && !canceled)“, was schnell unhaltbar wird.
FSMs verkörpern SoC und DRY vollständig. Imperative Programmierung nicht.
Bester JavaScript-Artikel seit langem. Bringen Sie die FSM zurück. Danke!
Ausgezeichneter Artikel. Die OMG hat die Semantik für FSMs hier sehr gut definiert: http://www.omg.org/spec/PSSM
Ein sehr gutes Werkzeug für die Entwicklung von FSMs und UML ist http://staruml.io/
Man könnte auch den Code für diese aus diesem Werkzeug generieren.
Vorsicht!!!! Sobald Sie FSMs verstehen, werden Sie sich verlieben.
Toller Artikel. Sie sagen, dass dies auf MobX-Stores angewendet werden kann; es wäre großartig, einen Mini-/Folgeartikel zu sehen.
Wird es ein REDUX-Beispiel geben? Das wäre süß.
Hallo, toller Artikel! Was ist der Zweck von style={{‘–i’: i}}
Gute Frage! Das ist eine Inline-CSS-Variable, die jetzt in den neuesten Versionen von React unterstützt wird.