— english, tutorial, React, XState, Auth — 4 min read
Photo by Matt Duncan on Unsplash
TLDR: checkout the final machine here
UPDATE: I rename the
callback
tosend
as a suggestion from Erik and I mention some resources created by Matt Pocock
One of the key libraries we use to develop the Mintter app is XState. In this short post, I want to show you how are we using it to check if a user is logged in or logged out, and change the app behaviour depending on the result.
XState is a library for creating, interpreting, and executing finite state machines and statecharts, as well as managing invocations of those machines as actors
I took this definition from the official website, and if you are not familiar with the concepts finite state machines or actors don't worry, it's not as complex as it sounds!
You can expand your knowledge about it in the official documentation, but in short, XState helps you define in a declarative way all the business logic of your application, making it easy to intercept, interact and respond to it with full confidence. Honestly, is one of the most important discoveries I've personally made on programming recently.
Not only helps you define your business logic in a more concise way, but also helps you communicate better with designers and other non-developer teammates, making your product more robust and future-proof. You should definitely give it a try!
if you don't have much time to read all the explanation, you can checkout the machine here
In Mintter, everytime a user opens the desktop app, we need to check if an account is present on the machine or not. Let's start by defining all the requirements we need to implement:
In this post we are just covering this part of the flow, the other parts will be covered in future blog posts!
From the top list you can see that we defined 3 states in which the user can be at any point in time. let's see the list again:
we can then rename this states to something more meaningful like:
So far so good!, let's also visualize this with a diagram:
and here's the code that creates this diagram:
1import { createModel } from "xstate/lib/model"23const authModel = createModel({4 account: undefined as string | undefined,5})67const authMachine = authModel.createMachine({8 initial: "checkingAccount",9 states: {10 checkingAccount: {},11 loggedIn: {},12 loggedOut: {},13 },14})
By the way, what we are creating is a machine, more specifically: a Finite State Machine, because we are defining a finite number of states that this machine can be and transition to. (read more about it here)
In order to transition from one state to another, we need to define a couple of events. The machine can receive as many events as you like, but each state needs to define which events wants to respond to.
For this machine, we want to transition from checkingAccount
to loggedIn
if the account is present, we can call this event REPORT_ACCOUNT_PRESENT
. if the account is not present, then we want to transition to loggedOut
: we can call this event REPORT_ACCOUNT_MISSING
. We also need LOG_IN
and LOG_OUT
events for the basic actions from the app. Let's add the events to our model:
1import { createModel } from "xstate/lib/model"23const authModel = createModel(4 {5 account: undefined as string | undefined,6 },7 {8 events: {9 LOGGED_IN: (account: string) => ({ account }),10 LOGGED_OUT: () => ({}),11 REPORT_ACCOUNT_PRESENT: (account: string) => ({ account }),12 REPORT_ACCOUNT_MISSING: () => ({}),13 },14 }15)1617const authMachine = authModel.createMachine({18 initial: "checkingAccount",19 states: {20 checkingAccount: {},21 loggedIn: {},22 loggedOut: {},23 },24})
I'm using the
createModel
utility because it gives better type safety when using the machine. you can use the commoncreateMachine
if you like too!
For this Auth flow, we want to inmediately make a request to the backend and check if the user is available or not, and a common and recommended way to do so is by invoking a Service or Actor. Invoking a service is not different from calling an asynchronous function to get some data.
As you can see in the above snippet, we are starting our machine on the checkingAccount
state, this means that we need to invoke our call to the API inside that state like so:
1// ...23const authMachine = authModel.createMachine({4 initial: "checkingAccount",5 states: {6 checkingAccount: {7 invoke: {8 id: "authMachine-fetch",9 src: "fetchAccount",10 },11 },12 loggedIn: {},13 loggedOut: {},14 },15})
The fetchAccount
service or Actor will be executed right after the machine enters the checkingAccount
state. It is recommended to define your services and actions inside the machine as strings, and implement them in the second parameter of the createMachine
function or by extending the machine with a withConfig
call (see more here). Let's implement our actor now:
1// ...23const authMachine = authModel.createMachine(4 {5 initial: "checkingAccount",6 states: {7 checkingAccount: {8 invoke: {9 id: "authMachine-fetch",10 src: "fetchAccount",11 },12 },13 loggedIn: {},14 loggedOut: {},15 },16 },17 {18 services: {19 fetchAccount: () => (send) => {20 return getAccount()21 .then(function (info) {22 send({ type: "REPORT_ACCOUNT_PRESENT", account })23 })24 .catch(function (err) {25 send("REPORT_ACCOUNT_MISSING")26 })27 },28 },29 }30)3132function getAccount() {33 return Promise.resolve("THE ACCOUNT")34}
Let's talk about the actual implementation of fetchAccount
. As you can see is a curried function, the first function gets as parameters the machine's context and the event, but for this actor we don't need those so we avoid them. the second function takes to arguments too, the first one is a send
and the second one is an event listener called onReceive
send
let you send events to the parent machineonReceive
let you listen to events sent to the parentIn our machine, the parent is the actual auth machine we are defining, since this is all defined from the actor perspective (the fetchAccount
function). having access to this send
is what we need to transition to the appropiate states depending on the request result! As you can see in the implementation, we are calling the send
passing the appropiate event based on the fetchAccount
result.
If you want to learn more about the Invoked Callback pattern, you can checkout this post from Matt Pocock
and that's it!, with this machine in place, we can then render the appropiate components based on the current state in which the machine is in, feeling very confident we are not going to get any weird errors or wrong renderings.
Or course this is a very small subset of all the machines and events we implement in our app, I will continue sharing more and more about how XState is helping us building our app with confidence and ease.
Here's the final Diagram and code. You can also play and fork this machine in the Stately registry (feel free to like it too!). Here's also a very simple implementation of the machine in a React Application.
1import { createModel } from "xstate/lib/model"23export const authModel = createModel(4 {5 account: undefined as string | undefined,6 },7 {8 events: {9 LOGGED_IN: (account: string) => ({ account }),10 LOGGED_OUT: () => ({}),11 REPORT_ACCOUNT_PRESENT: (account: string) => ({ account }),12 REPORT_ACCOUNT_MISSING: () => ({}),13 },14 }15)1617export const authStateMachine = authModel.createMachine(18 {19 id: "authStateMachine",20 context: authModel.initialContext,21 initial: "checkingAccount",22 states: {23 checkingAccount: {24 invoke: {25 id: "authMachine-fetch",26 src: "fetchAccount",27 },28 on: {29 REPORT_ACCOUNT_PRESENT: {30 target: "loggedIn",31 actions: [32 authModel.assign({33 account: (_, ev) => ev.account,34 }),35 ],36 },37 REPORT_ACCOUNT_MISSING: {38 target: "loggedOut",39 actions: [40 authModel.assign({41 account: undefined,42 }),43 ],44 },45 },46 },47 loggedIn: {},48 loggedOut: {},49 },50 },51 {52 services: {53 fetchAccount: () => (send) => {54 return getAccount()55 .then(function (account) {56 send({ type: "REPORT_ACCOUNT_PRESENT", account })57 })58 .catch(function (err) {59 send("REPORT_ACCOUNT_MISSING")60 })61 },62 },63 }64)6566function getAccount() {67 return Promise.resolve("THE ACCOUNT")68}
Thanks for reading until here!, if you have any comments or feedback please reach out via twitter!