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
}
}
);
```