Using React und XState zum Erstellen eines Anmeldeformulars

Avatar of Brad Woods
Brad Woods am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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

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:

  • focusEmailInput
  • focusPasswordInput
  • focusSubmitBtn
  • cacheEmail
  • cachePassword
  • onAuthentication

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:

  • isBadEmailFormat
  • isPasswordShort
  • isNoAccount
  • isIncorrectPassword
  • isServiceErr

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