4
\$\begingroup\$

I have a sample React app that loads a username and password, and then randomly provides a login result on submission for the sake of example.

CodeSandbox: https://codesandbox.io/s/stack-xstate-login-review-brvuu

This state machine makes use of always to check and transition between states (e.g., when validating usernames, passwords, and the API data). I'm just not sure if this is the correct way to evaluate data given that the visualizer does not show the transition logic in a clear manner.

Visualizer: https://xstate.js.org/viz/?gist=b49d6fe8e8ab83a91ad5138ff6e71f29

Is there a better way to evaluate data than bootlegging the always conditionals (e.g., with actions perhaps?)?

And of course, if you have any other feedback about the construction of this state machine, I very much welcome your input!

Full state machine code:

import { Machine, assign } from "xstate";
const usernameStates = {
 initial: "idle",
 states: {
 idle: {},
 valid: {},
 invalid: {
 states: {
 empty: {}
 }
 },
 validating: {
 always: [
 // empty transition name auto-runs its actions on state entry
 { cond: "isUsernameEmpty", target: "invalid.empty" },
 { target: "valid" }
 ]
 }
 }
};
const passwordStates = {
 initial: "idle",
 states: {
 idle: {},
 valid: {},
 invalid: {
 states: {
 empty: {}
 }
 },
 validating: {
 always: [
 // empty transition name auto-runs its actions on state entry
 { cond: "isPasswordEmpty", target: "invalid.empty" },
 { target: "valid" }
 ]
 }
 }
};
export const LoginFormStateMachine = Machine(
 {
 id: "loginFormState",
 initial: "input",
 context: {
 values: {
 username: "",
 password: "",
 resultLogin: {}
 }
 },
 states: {
 input: {
 type: "parallel",
 on: {
 CHANGE: [
 {
 cond: "isUsername",
 actions: "cacheValue",
 target: "input.username.validating"
 },
 {
 cond: "isPassword",
 actions: "cacheValue",
 target: "input.password.validating"
 }
 ],
 CLEAR: { actions: "resetForm", target: "input" },
 SUBMIT: { target: "validatingUser" }
 },
 states: {
 username: { ...usernameStates },
 password: { ...passwordStates }
 }
 },
 validatingUser: {
 invoke: {
 src: (context) =>
 fetch(
 "https://fakerapi.it/api/v1/custom?random=boolean"
 ).then((res) => res.json()),
 onDone: {
 actions: "cacheLoginResult",
 target: "checking"
 },
 onError: { target: "error" }
 }
 },
 checking: {
 always: [
 { cond: "isRecordLegit", target: "success" },
 { cond: "isRecordNotLegit", target: "error.mismatchingRecord" }
 ]
 },
 error: {
 states: {
 mismatchingRecord: { type: "final" }
 }
 },
 success: {
 type: "final"
 }
 }
 },
 {
 actions: {
 cacheValue: assign({
 values: (context, event) => ({
 ...context.values,
 [event.key]: event.value
 })
 }),
 cacheLoginResult: assign({
 values: (context, event) => ({
 ...context.values,
 // this particular data structure is based on the result
 // of the fetch request on Line 82, go to that URL to
 // see how this function randomly provides a login result
 resultLogin: event.data.data[0].random
 })
 }),
 resetForm: assign({ values: (context) => ({}) })
 },
 guards: {
 isUsername: (context, event) => event.key === "username",
 isPassword: (context, event) => event.key === "password",
 isUsernameEmpty: (context) => context.values.username.length === 0,
 isPasswordEmpty: (context) => context.values.password.length === 0,
 isRecordLegit: (context, event) => context.values.resultLogin,
 isRecordNotLegit: (context, event) => !context.values.resultLogin
 }
 }
);
```
asked Sep 15, 2020 at 3:30
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.