Update 15. Juni 2019
Seitdem dieser Artikel geschrieben wurde, hat es eine Reihe von Änderungen an XState gegeben. Eine aktualisierte Version eines Anmeldeformulars mit React & XState finden Sie hier.
Um ein Anmeldeformular mit guter Benutzererfahrung zu erstellen, ist UI-Zustandsverwaltung erforderlich. Das bedeutet, wir möchten die kognitive Belastung zur Vervollständigung minimieren und die Anzahl der erforderlichen Benutzeraktionen reduzieren, während wir gleichzeitig eine intuitive Erfahrung schaffen. Denken Sie darüber nach: Selbst ein relativ einfaches Anmeldeformular für E-Mail und Passwort muss eine Reihe verschiedener Zustände verwalten, wie z. B. leere Felder, Fehler, Passwortanforderungen, Ladevorgang und Erfolg.
Glücklicherweise ist die Zustandsverwaltung das, wofür React entwickelt wurde, und ich konnte ein Anmeldeformular damit erstellen, indem ich einen Ansatz mit XState verwendete, einer JavaScript-Bibliothek zur Zustandsverwaltung, die endliche Automaten nutzt.
Zustandsverwaltung? Endliche Automaten? Wir werden diese Konzepte gemeinsam durchgehen, während wir ein solides Anmeldeformular zusammenstellen.
Springen wir nach vorne, hier ist, was wir gemeinsam bauen werden

Zuerst, lass uns einrichten
Wir brauchen ein paar Werkzeuge, bevor wir anfangen. Hier ist, was Sie sich schnappen sollten
- Eine UI-Bibliothek: React
- Eine Styling-Bibliothek: styled-components
- Eine Zustandsverwaltungsbibliothek: XState
Sobald diese zur Hand sind, können wir sicherstellen, dass unser Projektordner für die Entwicklung eingerichtet ist. Hier ist ein Überblick darüber, wie die Dateien strukturiert sein sollten
public/
|--src/
|--Loader/
|--SignIn/
|--contactAuthService.js
|--index.jsx
|--isPasswordShort.js
|--machineConfig.js
|--styles.js
|--globalStyles.js
|--index.jsx
package.json
Ein wenig Hintergrund zu XState
Wir haben bereits erwähnt, dass XState eine JavaScript-Bibliothek zur Zustandsverwaltung ist. Sein Ansatz verwendet endliche Zustandsautomaten, was es ideal für diese Art von Projekt macht. Zum Beispiel:
- Es ist ein gründlich erprobter und getesteter Ansatz zur Zustandsverwaltung. Endliche Zustandsautomaten gibt es seit über 30 Jahren.
- Er ist gemäß einer Spezifikation aufgebaut.
- Es ermöglicht eine vollständige Trennung der Logik von der Implementierung, wodurch sie leicht testbar und modular wird.
- Es verfügt über einen visuellen Interpreter, der großartiges Feedback über den geschriebenen Code gibt und die Kommunikation des Systems mit einer anderen Person erleichtert.
Für weitere Informationen zu endlichen Zustandsautomaten lesen Sie David Khourshids Artikel.
Maschinenkonfiguration
Die Maschinenkonfiguration ist das *Herzstück* von XState. Es ist ein Statechart und definiert die *Logik* unseres Formulars. Ich habe sie in die folgenden Teile unterteilt, die wir nacheinander durchgehen werden.
1. Die Zustände
Wir brauchen eine Möglichkeit, zu steuern, was angezeigt, versteckt, aktiviert und deaktiviert wird. Wir steuern dies mit benannten Zuständen, die Folgendes umfassen:
dataEntry: Dies ist der Zustand, in dem der Benutzer eine E-Mail-Adresse und ein Passwort in die bereitgestellten Felder eingeben kann. Wir können dies als Standardzustand betrachten. Das aktuelle Feld wird blau hervorgehoben.

awaitingResponse: Dies geschieht, nachdem der Browser eine Anfrage an den Authentifizierungsdienst gesendet hat und wir auf die Antwort warten. Wir deaktivieren das Formular und ersetzen den Button durch eine Ladeanzeige, wenn sich das Formular in diesem Zustand befindet.

emailErr: Hoppla! Dieser Zustand wird ausgelöst, wenn es ein Problem mit der vom Benutzer eingegebenen E-Mail-Adresse gibt. Wir heben dieses Feld hervor, zeigen den Fehler an und deaktivieren das andere Feld und den Button.

passwordErr: Dies ist ein weiterer Fehlerzustand, diesmal, wenn es ein Problem mit dem vom Benutzer eingegebenen Passwort gibt. Wie beim vorherigen Fehler heben wir das Feld hervor, zeigen den Fehler an und deaktivieren den Rest des Formulars.

serviceErr: Wir erreichen diesen Zustand, wenn wir den Authentifizierungsdienst nicht kontaktieren können, was verhindert, dass die eingegebenen Daten überprüft werden. Wir zeigen einen Fehler zusammen mit einem "Erneut versuchen"-Button an, um die Dienstverbindung erneut herzustellen.

signedIn: Erfolg! Dies ist der Fall, wenn der Benutzer erfolgreich authentifiziert wurde und über das Anmeldeformular hinausgeht. Normalerweise würde dies den Benutzer zu einer bestimmten Ansicht führen, aber wir werden die Authentifizierung einfach bestätigen, da wir uns ausschließlich auf das Formular konzentrieren.

Sehen Sie die Datei machineConfig.js im Verzeichnis SignIn? Öffnen Sie sie, damit wir unsere Zustände definieren können. Wir listen sie als Eigenschaften eines states-Objekts auf. Wir müssen auch einen Anfangszustand definieren, der, wie bereits erwähnt, der dataEntry-Zustand sein wird, der es dem Benutzer ermöglicht, Daten in die Formularfelder einzugeben.
const machineConfig = {
id: 'signIn',
initial: 'dataEntry',
states: {
dataEntry: {},
awaitingResponse: {},
emailErr: {},
passwordErr: {},
serviceErr: {},
signedIn: {},
}
}
export default machineConfig

Jeder Teil dieses Artikels zeigt den Code von machineConfig.js zusammen mit einem Diagramm, das mit dem visuellen Tool von XState aus dem Code erstellt wurde Visualizer.
2. Die Übergänge
Nachdem wir unsere Zustände definiert haben, müssen wir definieren, wie man von einem Zustand zum anderen wechselt. In XState tun wir das mit einer Art von Ereignis namens *Übergang*. Wir definieren Übergänge innerhalb jedes Zustands. Wenn beispielsweise der Übergang ENTER_EMAIL ausgelöst wird, während wir uns im Zustand emailErr befinden, wechselt das System zum Zustand dataEntry.
emailErr: {
on: {
ENTER_EMAIL: {
target: 'dataEntry'
}
}
}
Beachten Sie, dass nichts passieren würde, wenn ein anderer Übergangstyp (z. B. ENTER_PASSWORD) im Zustand emailErr ausgelöst würde. Nur die innerhalb des Zustands definierten Übergänge sind gültig.
Wenn ein Übergang kein Ziel hat, ist es ein externer (standardmäßig) Selbstübergang. Wenn er ausgelöst wird, verlässt der Zustand sich selbst und tritt erneut ein. Als Beispiel wechselt die Maschine von dataEntry zurück zu dataEntry, wenn der Übergang ENTER_EMAIL ausgelöst wird.
Hier ist, wie das definiert ist
dataEntry: {
on: {
ENTER_EMAIL: {}
}
}
Klingt seltsam, ich weiß, aber wir werden es später erklären. Hier ist die machineConfig.js-Datei bisher.
const machineConfig = {
id: 'signIn',
initial: 'dataEntry',
states: {
dataEntry: {
on: {
ENTER_EMAIL: {},
ENTER_PASSWORD: {},
EMAIL_BLUR: {},
PASSWORD_BLUR: {},
SUBMIT: {
target: 'awaitingResponse',
},
},
},
awaitingResponse: {},
emailErr: {
on: {
ENTER_EMAIL: {
target: 'dataEntry',
},
},
},
passwordErr: {
on: {
ENTER_PASSWORD: {
target: 'dataEntry',
},
},
},
serviceErr: {
on: {
SUBMIT: {
target: 'awaitingResponse',
},
},
},
signedIn: {},
},
};
export default machineConfig;

3. Kontext
Wir brauchen eine Möglichkeit, das, was der Benutzer in die Eingabefelder eingibt, zu speichern. Das können wir in XState mit dem Kontext tun, einem Objekt innerhalb der Maschine, das uns ermöglicht, Daten zu speichern. Wir müssen das also auch in unserer Datei definieren.
E-Mail und Passwort sind standardmäßig beides leere Strings. Wenn der Benutzer seine E-Mail-Adresse oder sein Passwort eingibt, speichern wir es hier.
const machineConfig = {
id: 'signIn',
context: {
email: '',
password: '',
},
...
4. Hierarchische Zustände
Wir brauchen eine Möglichkeit, spezifischer auf unsere Fehler einzugehen. Anstatt dem Benutzer einfach zu sagen, dass es einen E-Mail-Fehler gibt, müssen wir ihm mitteilen, welche Art von Fehler aufgetreten ist. Vielleicht hat die E-Mail ein falsches Format oder es ist keine E-Mail mit der eingegebenen Adresse verknüpft – wir sollten den Benutzer informieren, damit er nicht raten muss. Hier können wir hierarchische Zustände verwenden, die im Wesentlichen Zustandsautomaten innerhalb von Zustandsautomaten sind. Anstatt also einen emailErr-Zustand zu haben, können wir Unterzustände hinzufügen, wie z. B. emailErr.badFormat oder emailErr.noAccount.
Für den Zustand emailErr haben wir zwei Unterzustände definiert: badFormat und noAccount. Das bedeutet, dass die Maschine nicht mehr nur im Zustand emailErr sein kann; sie wäre entweder im Zustand emailErr.badFormat oder im Zustand emailErr.noAccount. Das Aufteilen ermöglicht es uns, dem Benutzer mehr Kontext in Form von eindeutigen Meldungen in jedem Unterzustand zu geben.
const machineConfig = {
...
states: {
...
emailErr: {
on: {
ENTER_EMAIL: {
target: 'dataEntry',
},
},
initial: 'badFormat',
states: {
badFormat: {},
noAccount: {},
},
},
passwordErr: {
on: {
ENTER_PASSWORD: {
target: 'dataEntry',
},
},
initial: 'tooShort',
states: {
tooShort: {},
incorrect: {},
},
},
...

5. Guards
Wenn der Benutzer ein Eingabefeld verlässt oder auf Senden klickt, müssen wir prüfen, ob die E-Mail-Adresse und/oder das Passwort gültig sind. Wenn auch nur einer dieser Werte ein falsches Format hat, müssen wir den Benutzer auffordern, ihn zu ändern. Guards ermöglichen es uns, basierend auf solchen Bedingungen in einen Zustand zu wechseln.
Hier verwenden wir den Übergang EMAIL_BLUR, um den Zustand zu emailErr.badFormat zu ändern, nur wenn die Bedingung isBadEmailFormat wahr zurückgibt. Wir tun etwas Ähnliches für PASSWORD_BLUR.
Wir ändern auch den Wert des Übergangs SUBMIT in ein Array von Objekten mit den Eigenschaften target und condition. Wenn der Übergang SUBMIT ausgelöst wird, durchläuft die Maschine jede der Bedingungen von der ersten bis zur letzten und ändert den Zustand auf die erste Bedingung, die wahr zurückgibt. Wenn beispielsweise isBadEmailFormat wahr zurückgibt, wechselt die Maschine zum Zustand emailErr.badFormat. Wenn isBadEmailFormat jedoch falsch zurückgibt, wechselt die Maschine zur nächsten Bedingung und prüft, ob sie wahr zurückgibt.
const machineConfig = {
...
states: {
...
dataEntry: {
...
on: {
EMAIL_BLUR: {
cond: 'isBadEmailFormat',
target: 'emailErr.badFormat'
},
PASSWORD_BLUR: {
cond: 'isPasswordShort',
target: 'passwordErr.tooShort'
},
SUBMIT: [
{
cond: 'isBadEmailFormat',
target: 'emailErr.badFormat'
},
{
cond: 'isPasswordShort',
target: 'passwordErr.tooShort'
},
{
target: 'awaitingResponse'
}
],
...

6. Invoke
Die bisher geleistete Arbeit wäre vergeblich, wenn wir keine Anfrage an einen Authentifizierungsdienst stellen würden. Das Ergebnis dessen, was in das Formular eingegeben und gesendet wird, informiert viele der definierten Zustände. Daher sollte der Aufruf dieser Anfrage zu einem von zwei Zuständen führen:
- Übergang in den Zustand
signedIn, wenn die Antwort erfolgreich ist, oder - Übergang in einen unserer Fehlerzustände, wenn sie fehlschlägt.
Die invoke-Methode ermöglicht es uns, ein Promise zu deklarieren und je nach Ergebnis des Promises in verschiedene Zustände zu wechseln. Die Eigenschaft src nimmt eine Funktion entgegen, die zwei Parameter hat: context und event (aber wir verwenden hier nur context). Wir geben ein Promise zurück (unsere Authentifizierungsanfrage) mit den Werten für E-Mail und Passwort aus dem Kontext. Wenn das Promise erfolgreich zurückgegeben wird, wechseln wir in den in der Eigenschaft onDone definierten Zustand. Wenn ein Fehler zurückgegeben wird, wechseln wir in den in der Eigenschaft onError definierten Zustand.
const machineConfig = {
...
states: {
...
// We’re in a state of waiting for a response
awaitingResponse: {
// Make a call to the authentication service
invoke: {
src: 'requestSignIn',
// If successful, move to the signedIn state
onDone: {
target: 'signedIn'
},
// If email input is unsuccessful, move to the emailErr.noAccount sub-state
onError: [
{
cond: 'isNoAccount',
target: 'emailErr.noAccount'
},
{
// If password input is unsuccessful, move to the passwordErr.incorrect sub-state
cond: 'isIncorrectPassword',
target: 'passwordErr.incorrect'
},
{
// If the service itself cannot be reached, move to the serviceErr state
cond: 'isServiceErr',
target: 'serviceErr'
}
]
},
},
...

7. Aktionen
Wir brauchen eine Möglichkeit, das zu speichern, was der Benutzer in die E-Mail- und Passwortfelder eingibt. Aktionen ermöglichen es, Seiteneffekte auszulösen, wenn ein Übergang auftritt. Unten haben wir eine Aktion (cacheEmail) innerhalb des Übergangs ENTER_EMAIL des Zustands dataEntry definiert. Das bedeutet, wenn sich die Maschine in dataEntry befindet und der Übergang ENTER_EMAIL ausgelöst wird, wird auch die Aktion cacheEmail ausgelöst.
const machineConfig = {
...
states: {
...
// On submit, target the two fields
dataEntry: {
on: {
ENTER_EMAIL: {
actions: 'cacheEmail'
},
ENTER_PASSWORD: {
actions: 'cachePassword'
},
},
...
},
// If there’s an email error on that field, trigger email cache action
emailErr: {
on: {
ENTER_EMAIL: {
actions: 'cacheEmail',
...
}
}
},
// If there’s a password error on that field, trigger password cache action
passwordErr: {
on: {
ENTER_PASSWORD: {
actions: 'cachePassword',
...
}
}
},
...

8. Endzustand
Wir brauchen eine Möglichkeit, anzuzeigen, ob der Benutzer erfolgreich authentifiziert wurde, und je nach Ergebnis die nächste Stufe der Benutzerreise auszulösen. Dafür sind zwei Dinge erforderlich:
- Wir deklarieren, dass einer der Zustände der Endzustand ist, und
- definieren eine Eigenschaft
onDone, die Aktionen auslösen kann, wenn dieser Endzustand erreicht wird.
Innerhalb des Zustands signedIn fügen wir type: final hinzu. Wir fügen auch eine Eigenschaft onDone mit der Aktion onAuthentication hinzu. Wenn nun der Zustand signedIn erreicht wird, wird die Aktion onAuthentication ausgelöst und die Maschine ist *fertig* (nicht mehr ausführbar).
const machineConfig = {
...
states: {
...
signedIn: {
type: 'final'
},
onDone: {
actions: 'onAuthentication'
},
...

9. Test
Ein großartiges Merkmal von XState ist, dass die Maschinenkonfiguration vollständig von der tatsächlichen Implementierung getrennt ist. Das bedeutet, wir können sie jetzt testen und uns von dem, was wir gemacht haben, überzeugen, bevor wir sie mit der Benutzeroberfläche und dem Backend-Dienst verbinden. Wir können die Datei der Maschinenkonfiguration in das visuelle Tool von XState kopieren und einfügen Visualizer und erhalten ein automatisch generiertes Statechart-Diagramm, das nicht nur alle definierten Zustände mit Pfeilen darstellt, die zeigen, wie sie alle miteinander verbunden sind, sondern auch die Interaktion mit dem Diagramm ermöglicht. Das ist integriertes Testen!

Die Maschine mit einer React-Komponente verbinden
Nachdem wir nun unsere Statechart geschrieben haben, ist es an der Zeit, sie mit unserer Benutzeroberfläche und unserem Backend-Dienst zu verbinden. Ein XState-Maschinen-Options-Objekt ermöglicht es uns, die im Konfigurationsstring deklarierten Elemente Funktionen zuzuordnen.
Wir beginnen mit der Definition einer React-Klassenkomponente mit drei Refs
// SignIn/index.jsx
import React, { Component, createRef } from 'react'
class SignIn extends Component {
emailInputRef = createRef()
passwordInputRef = createRef()
submitBtnRef = createRef()
render() {
return null
}
}
export default SignIn
Aktionen abbilden
Wir haben die folgenden Aktionen in unserer Maschinenkonfiguration deklariert:
focusEmailInputfocusPasswordInputfocusSubmitBtncacheEmailcachePasswordonAuthentication
Aktionen werden in der Eigenschaft actions der Maschinenkonfiguration zugeordnet. Jede Funktion nimmt zwei Argumente entgegen: den Kontext (ctx) und das Ereignis (evt).
focusEmailInput und focusPasswordInput sind ziemlich geradlinig, es gibt jedoch einen Fehler. Diese Elemente werden fokussiert, wenn sie aus einem deaktivierten Zustand kommen. Die Funktion zum Fokussieren dieser Elemente wird ausgeführt, kurz bevor die Elemente wieder aktiviert werden. Die Funktion delay umgeht dies.
cacheEmail und cachePassword müssen den Kontext aktualisieren. Dazu verwenden wir die Funktion assign (bereitgestellt von XState). Was auch immer von der assign-Funktion zurückgegeben wird, wird unserem Kontext hinzugefügt. In unserem Fall liest sie den Wert des Eingabefeldes aus dem Ereignisobjekt und fügt diesen Wert dann dem E-Mail- oder Passwortkontext hinzu. Von dort wird property.assign dem Kontext hinzugefügt. Wieder einmal liest es in unserem Fall den Wert des Eingabefeldes aus dem Ereignisobjekt und fügt diesen Wert der E-Mail- oder Passwort-Eigenschaft des Kontexts hinzu.
// SignIn/index.jsx
import { actions } from 'xstate'
const { assign } = actions
const delay = func => setTimeout(() => func())
class SignIn extends Component {
...
machineOptions = {
actions: {
focusEmailInput: () => {
delay(this.emailInputRef.current.focus())
},
focusPasswordInput: () => {
delay(this.passwordInputRef.current.focus())
},
focusSubmitBtn: () => {
delay(this.submitBtnRef.current.focus())
},
cacheEmail: assign((ctx, evt) => ({
email: evt.value
})),
cachePassword: assign((ctx, evt) => ({
password: evt.value
})),
// We’ll log a note in the console to confirm authentication
onAuthentication: () => {
console.log('user authenticated')
}
},
}
}
Stellen wir unsere Guards auf
Wir haben die folgenden Guards in unserer Maschinenkonfiguration deklariert:
isBadEmailFormatisPasswordShortisNoAccountisIncorrectPasswordisServiceErr
Guards werden in der Eigenschaft guards der Maschinenkonfiguration zugeordnet. Die Guards isBadEmailFormat und isPasswordShort verwenden den context, um die vom Benutzer eingegebene E-Mail-Adresse und das Passwort zu lesen, und geben sie dann an die entsprechenden Funktionen weiter. isNowAccount, isIncorrectPassword und isServiceErr verwenden das Ereignisobjekt, um zu lesen, welche Art von Fehler von dem Aufruf des Authentifizierungsdienstes zurückgegeben wurde.
// isPasswordShort.js
const isPasswordShort = password => password.length < 6
export default isPasswordShort
// SignIn/index.jsx
import { isEmail } from 'validator'
import isPasswordShort from './isPasswordShort'
class SignIn extends Component {
...
machineOptions = {
...
guards: {
isBadEmailFormat: ctx => !isEmail(ctx.email),
isPasswordShort: ctx => isPasswordShort(ctx.password),
isNoAccount: (ctx, evt) => evt.data.code === 1,
isIncorrectPassword: (ctx, evt) => evt.data.code === 2,
isServiceErr: (ctx, evt) => evt.data.code === 3
},
},
...
}
Die Dienste anschließen
Wir haben den folgenden Dienst in unserer Maschinenkonfiguration deklariert (innerhalb unserer invoke-Definition): requestSignIn.
Dienste werden in der Eigenschaft services der Maschinenkonfiguration zugeordnet. In diesem Fall ist die Funktion ein Promise und wird aus dem *Kontext* an die E-Mail- und Passwort-Felder übergeben.
// contactAuthService.js
// error code 1 - no account
// error code 2 - wrong password
// error code 3 - no response
const isSuccess = () => Math.random() >= 0.8
const generateErrCode = () => Math.floor(Math.random() * 3) + 1
const contactAuthService = (email, password) =>
new Promise((resolve, reject) => {
console.log(`email: ${email}`)
console.log(`password: ${password}`)
setTimeout(() => {
if (isSuccess()) resolve()
reject({ code: generateErrCode() })
}, 1500)
})
export default contactAuthService
// SignIn/index.jsx
...
import contactAuthService from './contactAuthService.js'
class SignIn extends Component {
...
machineOptions = {
...
services: {
requestSignIn: ctx => contactAuthService(ctx.email, ctx.password)
}
},
...
}
react-xstate-js verbindet React und XState
Jetzt, da wir unsere Maschinenkonfiguration und Optionen bereit haben, können wir die eigentliche Maschine erstellen! Um XState in einem *realen* Szenario zu verwenden, ist ein Interpreter erforderlich. react-xstate-js ist ein Interpreter, der React mit XState über den Render Props-Ansatz verbindet. (Volle Offenlegung, ich habe diese Bibliothek entwickelt.) Sie nimmt zwei Props entgegen – config und options – und gibt ein XState service- und state-Objekt zurück.
// SignIn/index.jsx
...
import { Machine } from 'react-xstate-js'
import machineConfig from './machineConfig'
class SignIn extends Component {
...
render() {
<Machine config={machineConfig} options={this.machineOptions}>
{({ service, state }) => null}
</Machine>
}
}
Lasst uns die Benutzeroberfläche erstellen!
OK, wir haben eine funktionierende Maschine, aber der Benutzer muss das Formular sehen, um es benutzen zu können. Das bedeutet, es ist Zeit, die Markup für die UI-Komponente zu erstellen. Es gibt zwei Dinge, die wir tun müssen, um mit unserer Maschine zu kommunizieren:
1. Den Zustand lesen
Um zu bestimmen, in welchem Zustand wir uns befinden, können wir die matches-Methode des Zustands verwenden und einen booleschen Wert zurückgeben. Zum Beispiel: state.matches('dataEntry').
2. Einen Übergang auslösen
Um einen Übergang auszulösen, verwenden wir die send-Methode des Dienstes. Sie nimmt ein Objekt entgegen, das den Typ des zu auslösenden Übergangs sowie alle anderen Schlüssel-Wert-Paare enthält, die wir im evt-Objekt haben möchten. Zum Beispiel: service.send({ type: 'SUBMIT' }).
// SignIn/index.jsx
...
import {
Form,
H1,
Label,
Recede,
Input,
ErrMsg,
Button,
Authenticated,
MetaWrapper,
Pre
} from './styles'
class SignIn extends Component {
...
render() {
<Machine config={machineConfig} options={this.machineOptions}>
{({ service, state }) => {
const disableEmail =
state.matches('passwordErr') ||
state.matches('awaitingResponse') ||
state.matches('serviceErr')
const disablePassword =
state.matches('emailErr') ||
state.matches('awaitingResponse') ||
state.matches('serviceErr')
const disableSubmit =
state.matches('emailErr') ||
state.matches('passwordErr') ||
state.matches('awaitingResponse')
const fadeHeading =
state.matches('emailErr') ||
state.matches('passwordErr') ||
state.matches('awaitingResponse') ||
state.matches('serviceErr')
return (
<Form
onSubmit={e => {
e.preventDefault()
service.send({ type: 'SUBMIT' })
}}
noValidate
>
<H1 fade={fadeHeading}>Welcome Back</H1>
<Label htmlFor="email" disabled={disableEmail}>
email
</Label>
<Input
id="email"
type="email"
placeholder="[email protected]"
onBlur={() => {
service.send({ type: 'EMAIL_BLUR' })
}}
value={state.context.email}
err={state.matches('emailErr')}
disabled={disableEmail}
onChange={e => {
service.send({
type: 'ENTER_EMAIL',
value: e.target.value
})
}}
ref={this.emailInputRef}
autoFocus
/>
<ErrMsg>
{state.matches({ emailErr: 'badFormat' }) &&
"email format doesn't look right"}
{state.matches({ emailErr: 'noAccount' }) &&
'no account linked with this email'}
</ErrMsg>
<Label htmlFor="password" disabled={disablePassword}>
password <Recede>(min. 6 characters)</Recede>
</Label>
<Input
id="password"
type="password"
placeholder="Passw0rd!"
value={state.context.password}
err={state.matches('passwordErr')}
disabled={disablePassword}
onBlur={() => {
service.send({ type: 'PASSWORD_BLUR' })
}}
onChange={e => {
service.send({
type: 'ENTER_PASSWORD',
value: e.target.value
})
}}
ref={this.passwordInputRef}
/>
<ErrMsg>
{state.matches({ passwordErr: 'tooShort' }) &&
'password too short (min. 6 characters)'}
{state.matches({ passwordErr: 'incorrect' }) &&
'incorrect password'}
</ErrMsg>
<Button
type="submit"
disabled={disableSubmit}
loading={state.matches('awaitingResponse')}
ref={this.submitBtnRef}
>
{state.matches('awaitingResponse') && (
<>
loading
<Loader />
</>
)}
{state.matches('serviceErr') && 'retry'}
{!state.matches('awaitingResponse') &&
!state.matches('serviceErr') &&
'sign in'
}
</Button>
<ErrMsg>
{state.matches('serviceErr') && 'problem contacting server'}
</ErrMsg>
{state.matches('signedIn') && (
<Authenticated>
<H1>authenticated</H1>
</Authenticated>
)}
</Form>
)
}}
</Machine>
}
}
Wir haben ein Formular!
Und da haben Sie es. Ein Anmeldeformular mit einer großartigen Benutzererfahrung, gesteuert von XState. Wir konnten nicht nur ein Formular erstellen, mit dem ein Benutzer interagieren kann, sondern haben auch viel über die vielen Zustände und Arten von Interaktionen nachgedacht, die berücksichtigt werden müssen, was eine gute Übung für jede Funktionalität ist, die in eine Komponente integriert wird.
Nutzen Sie das Kommentarformular, wenn etwas unklar ist oder wenn Sie der Meinung sind, dass im Formular noch etwas berücksichtigt werden müsste. Ich freue mich auf Ihre Gedanken!
Weitere Ressourcen
- XState Dokumentation
- react-xstate-js Repository
- Finite State Machine mit React von Jon Bellah (großartig für nächste Schritte, um unsere endlichen Maschinen aufzurüsten)
Warum sollte man XState gegenüber Redux verwenden? Bietet es etwas, das Redux nicht einfach kann?
Ich war zögerlich, als ich XState zum ersten Mal aus der Redux-Perspektive sah, aber ich habe festgestellt, dass es viele bedeutende Vorteile bietet. Ich habe eine Präsentation gehalten, in der die beiden verglichen werden hier, die einige der wichtigsten behandelt.
Warum nicht ALLE Eingaben frühestens mit dem Klick auf Senden prüfen und ALLE Fehler gleichzeitig an den Benutzer zurückgeben? Das würde dem Benutzer mehr Kontext darüber geben, was falsch ist, und ihm mehr Freiheit geben, die Felder in beliebiger Reihenfolge und mit beliebigen Inhalten auszufüllen, die er mag.
Dies ist kein gutes Beispiel für die Verwendung eines Zustandsautomaten.
Ich würde mich sofort von einer so sturen, unflexiblen Benutzeroberfläche abwenden.
Danke für das Feedback. Für
In einem der letzten Absätze schrieben Sie „Es zwingt den Benutzer…“ und das ist
was ich für falsch halte. Was ich in den letzten 40 Jahren Softwareentwicklung gelernt habe, ist
„benutze einen Benutzer niemals…“!
Ich habe ein wenig mit Ihrem Login-Formular gespielt und es gibt mir tatsächlich eine kognitive Überlastung, weil es
Felder ein- und ausblendet, falls ein Fehler auftritt, und Fehler anzeigt, wo es nicht sein sollte.
Bei einem neuen, leeren Formular löst das Klicken irgendwo außerhalb der Felder, nicht auf ANMELDEN, einen Fehler „falsche E-Mail“ aus
und alle anderen Elemente sind verschwunden. Warum?
Als nächstes gibt das Eingeben einer gültigen E-Mail-Adresse und eines Passworts mit sechs Zeichen eine Fehlermeldung „falsches Passwort“. Warum?
state: {
„passwordErr“: „incorrect“
}
context (ctx): {
„email“: „[email protected]“,
„password“: „'\\“'\\“'\\“'“
}
alle anderen Elemente weg!
Wenn ich einen RETRY-Button bekomme, weil „Probleme beim Kontaktieren des Servers“
ist das gesamte Formular weg. Warum?
Ich habe mehrere andere Kombinationen ausprobiert und erhalte ein sehr erratisches Verhalten, manchmal
habe ich Erfolg und manchmal nicht.
Ich schätze Ihre Denkweise und Ihre Arbeit für eine gute Benutzererfahrung, aber um ehrlich zu sein,
diese Implementierung würde meine Qualitätsgrenze nicht bestehen.
Benutze niemals einen Benutzer…
Grüße
Heinz
Das ist die Regel der geringsten Macht am Werk. Aufgrund der deklarativen Natur von XState können die in der Maschine kodierten Informationen zur Visualisierung wiederverwendet werden.
Der Gedanke dahinter, Felder bei einem Fehler ein- und auszublenden, ist, den Benutzer auf das zu lenken, was er tun muss. Wenn es einen Fehler bei der E-Mail-Adresse gibt, muss er den Wert der E-Mail-Eingabe ändern, daher ist es nicht notwendig, dass die Passwort-Eingabe oder die Schaltfläche zum Absenden sichtbar sind.
In Bezug auf die Eingabe einer gültigen E-Mail-Adresse und eines sechs Zeichen langen Passworts ergibt 'schlechtes Passwort'. Warum? – Es gibt keine Fehlermeldung 'schlechtes Passwort'. Es gibt die Fehlermeldung 'falsches Passwort'. Dies geschieht, wenn der Mock-Authentifizierungsdienst aufgerufen wird. Er wählt zufällig eines der folgenden Szenarien aus (kein Konto mit gegebener E-Mail-Adresse verknüpft, falsches Passwort, Dienst nicht erreichbar oder erfolgreiche Authentifizierung). Ich habe es so gemacht, damit die Leute sehen können, wie jedes Szenario im Formular aussieht. Dies erklärt auch das von Ihnen erwähnte erratische Verhalten.
In Bezug auf Wenn ich einen RETRY-Button erhalte, weil 'Probleme beim Kontaktieren des Servers' auftreten, ist das gesamte Formular weg. Warum? – Dieser Fehlerzustand wird erreicht, wenn das Formular keinen Kontakt mit dem Authentifizierungsdienst herstellen kann. Es gibt keinen Grund für den Benutzer, in diesem Zustand seine E-Mail-Adresse oder sein Passwort zu ändern, da möglicherweise nichts mit ihnen nicht stimmt. Erst nachdem der Authentifizierungsdienst die E-Mail-Adresse und das Passwort überprüft und ein Problem festgestellt hat, wäre es notwendig, die Eingaben zu ändern. Daher sind die Eingaben ausgeblendet und eine Retry-Schaltfläche wird angezeigt, da dies die einzige Aktion ist, die der Benutzer ausführen muss, alles andere wäre überflüssig.
Ich habe es hier getestet, aber der onDone-Zustand scheint überhaupt nicht ausgeführt zu werden. Was könnte los sein?
Ich bin mir nicht sicher. Ich habe den Featured CodeSandbox gerade noch einmal überprüft und onDone hat bei mir funktioniert. Beachten Sie, dass onDone nur ausgelöst wird, wenn der Benutzer sich erfolgreich authentifiziert. Es kann einige Versuche dauern, bis Sie diesen Zustand erreichen, da ich die Antworten des Mock-Servers randomisiert habe.