Firebase authentication with XState and Svelte
Tags: firebase, svelte, xstate, webdev
Published: 2020-08-03 15:48:55

---
title: Firebase authentication with XState and Svelte
published: true
date: 2020-07-03 00:00:00 UTC
tags: firebase, svelte, xstate, webdev
canonical_url: https://codechips.me/firebase-authentication-with-xstate-and-svelte/
---
This is a follow-up to my previous article on Firebase authentication. I was not satisfied with the binary solution and decided to enhance it with XState.

It's hard to understand XState just with light bulb and red light examples. I need something concrete, something to chew on. A practical use case. That's why I asked myself if I could use XState for Firebase authentication.

XState is hard to learn. In order to understand the concepts you must actually sit down and type out the code.

Take 20 minutes of your busy schedule. It will be time well spent. I promise!

## What you will learn

Below is a list of the things you will learn:

- Creating XState machines
- Using XState in Svelte
- Encapsulating external libraries in XState
- Using Firebase authentication in XState

This is what the final result will look like.

![firebase authentication xstate login form](https://res.cloudinary.com/codechips/image/upload/v1596197355/posts/firebase-auth-xstate/firebase-authentication-login-form_xzz9xk.webp)

Onward.

## Requirements

Since this is a follow-up article to [Firebase authentication with Svelte](https://codechips.me/firebase-authentication-with-svelte/) we will base all the code on it. Read it first if you want to understand how Firebase authentication works.

## What is XState?

XState is a small (and lightweight) library that helps you manage state in your application. The gist of XState is that you application can only be in **one state at a time** and from that state you can only move to other predefined states by the rules you define. Because of that, it makes working with state very predictable, but notice I didn't say easy.

Even though, XState has one of the best [documentations](https://xstate.js.org/docs/) I have seen, it's not easy to understand all its concepts. You have to start with a blank state of mind and take your time to absorb all the new ideas it offers.

Many people think that XState can only be used in UI when working with forms, but that not true. You can use it anywhere you need predicable state.

## State machines briefly explained

I won't explain the XState in-depth, because XState's documentation does it 1000 times better that I ever could, but I want to briefly explain state machines.

State machines are the core of XState and you will be writing many when working with the XState library.

Here is a very basic and oversimplified example of an auth state machine just to get an idea on how to work with XState.

```js
import { createMachine, interpret } from 'xstate';

// create a state machine config
const config = {
  id: 'auth',
  initial: 'signedOut',
  states: {
    authenticating: {
      on: {
        'SUCCESS': 'signedIn',
        'FAILURE': 'signedOut'
      }
    },
    signedIn: {
      on: { 'LOGOUT': 'signedOut' }
    },
    signedOut: {
      on: { 'LOGIN': 'authenticating' }
    }
  }
};

// create a machine instance from config
const machine = createMachine(config);

// create an instance of an interpreted machine,
// also known as a service.
const service = interpret(machine)

// start our service
service.start();

service.state.value === 'signedOut';
// true

// send an event that initiates a state transition
// to the 'authenticating' state
service.send('LOGIN');

service.state.matches('authenticating');
// true

service.send('SUCCESS');

service.state.matches('signedIn');
// true

service.stop();
```

You can do many fancy things with state machines, as you will learn. The basics of the state machines - you make decisions on the current state by sending events to the instance of the state machine.

## Identifying all possible states

With our newly gained basic state machine knowledge, let's try to identify the states we have when it comes to our Firebase authentication.

We have three obvious states:

- **Authenticating**, when checking user's authentication status
- **Signed out**, when the user is logged out
- **Signed in**, when the user is logged in

But if we think hard about it we have two more states:

- **Signing in**, when the user is logging in
- **Signing out**, when the user is logging out

You might think they are actions, but they are actually states. At least in my eyes.

When I am trying to identify states I often start with an action, such as "log in", and try to convert action to state - "logging in". If it makes sense, then it's probably a state.

We can also have additional states that are related to authentication such as **loading**, where we enrich the user with additional data like roles and permissions.

With our naïve assumptions let's try and build an initial version of our state machine.

## Initial auth machine version

Let's get cranking! Create an `authMachine.js` file in the `src/auth` directory.

```js
const config = {
  id: 'auth',
  // we want to start by checking if
  // user is logged in when page loads
  initial: 'authenticating',
  // context is where you keep your app state
  context: {
    auth: null,
    error: null
  },
  // all possible authentication states
  states: {
    authenticating: {},
    // we will enrich the user profile
    // with additional data
    loading: {},
    signedIn: {
      // when receiving 'LOGOUT' event
      // transition to singingOut state
      on: { LOGOUT: { target: 'signingOut' } }
    },
    signedOut: {
      // when receiving 'LOGIN' event
      // transition to signingIn state
      on: { LOGIN: { target: 'signingIn' } }
    },
    signingIn: {},
    signingOut: {}
  }
};

```
That will do for now. But how do we check if the user is authenticated or not? That's where XState's [effects](https://xstate.js.org/docs/guides/effects.html) come in.

## Doing side effects in XState

In XState you can invoke different actions when you transition to a state. You can invoke callbacks, promises, observables and even other state machines.

The options are many, but in our example we will use promises to keep things simple. Because everyone knows how promises work, right?

When we want transition to a different state (by sending an event for example) we can invoke a XState service, a function that return promise in our case, with the `invoke` property.

When invoking the service XState will pass two arguments to it - machine's **context** and **event** that triggered the state transition.

Here is a pseudo-example of it could look for our `authenticating` state.

```js
states: {
  authenticating: {
    // when entering a state invoke
    // the service
    invoke: {
      id: 'checkAuth',
      // can be inline or we can refer to it
      // by an identifier as we will see later
      src: (ctx, event) => {
        console.log({ctx, event});
        return new Promise((resolve, reject) => {
          // simulate a slow auth method
          setTimeout(resolve, 1000);
        });
      },
      // called on Promise.resolve()
      onDone: {
        target: 'signedIn'
      },
      // called on Promise.reject()
      onError: {
        target: 'signedOut',
      }
    }
  },
  // ...
  signedIn: {},
  signedOut: {}
}
```
Are you with me so far? In the example above when our auth machine starts, it will start in the `authenticating` state and immediately invoke the promise with the `checkAuth` id.

The promise will in this case resolve and therefore call the `onDone` hook, which will transition from `authenticating` to `signedIn` state. In the `onDone` hook it will pass in the current app context and event as arguments.

One important concept to understand is that XState uses events to transition from one state to another. They are external and also internal stimulus that we can act on.

If you ever worked with Redux you can think of events as Redux actions. Every event always has a type, its name, and along side with that you can pass in data.

As an analogy we can think of `(context, event)` arguments as Redux `(state, action)` concept.

Don't know if its a fair comparison, but that's how I see it. More on that later.

## Wiring up the machine

To make things more clear let's play around a bit our auth machine in its current state and wire it up to few buttons that we can push.

First, we need to wrap it in a Svelte readable store. Create a `useMachine.js` file in `src/auth` directory.

```js
// useMachine.js

import { readable } from 'svelte/store';
import { interpret } from 'xstate';

export const useMachine = (config, options) => {
  // interpret the machine config
  const service = interpret(config, options);

  // wrap machine in a svelte readable store with
  // initial state (defined in config) as its starting state
  const store = readable(service.initialState, set => {
    // every time we change state onTransition
    // hook is triggered
    service.onTransition(state => {
      set(state);
    });

    // start the machine
    service.start();

    return () => {
      service.stop();
    };
  });

  // return a custom Svelte store
  return {
    state: store,
    send: service.send
  };
};
```

Before we can use machine we need to create it. Adjust the `authMachine.js` to following.

```js
// authMachine.js

import { createMachine } from 'xstate';

const config = {
  // ...
}

export const initAuthMachine = () => createMachine(config);
```

Now we are ready to use it in the our `auth/index.js` file. Let's change it to this for now.

```js
import firebase from 'firebase/app';
import 'firebase/auth';
import { useMachine } from './useMachine';
import { initAuthMachine } from './authMachine';

const userMapper = claims => ({
  id: claims.user_id,
  name: claims.name,
  email: claims.email,
  picture: claims.picture
});

export const initAuth = (useRedirect = false) => {
  const auth = firebase.auth();

  const loginWithEmailPassword = (email, password) =>
    auth.signInWithEmailAndPassword(email, password);

  const loginWithGoogle = () => {
    const provider = new firebase.auth.GoogleAuthProvider();

		return useRedirect
			? auth.signInWithRedirect(provider)
			: auth.signInWithPopup(provider);
  };

  const authMachine = initAuthMachine();

  return useMachine(authMachine);
};
```

There are a few unused functions, but don't worry, we will put them to use later.

We can finally wire up the machine in our `App.svelte` file. You can find the complete setup code for the app and styles for the main file in my previous tutorial on [Firebase authentication](https://codechips.me/firebase-authentication-with-svelte/).

```html
<!-- App.svelte with styles omitted -->

<script>
  import firebase from 'firebase/app';
  import { initAuth } from './auth';
  import { fade } from 'svelte/transition';

  const firebaseConfig = {
    // vite reads env variables from .env file in root dir
    apiKey: `${import.meta.env.VITE_FIREBASE_KEY}`,
    authDomain: 'testing-firebase-emulators.firebaseapp.com',
    projectId: 'testing-firebase-emulators'
  };

  firebase.initializeApp(firebaseConfig);

  // user custom auth machine store
  const { state, send } = initAuth();

  const loginHandler = async event => {
    const { email, password } = event.target.elements;
    // send login event
    send('LOGIN');
  };
</script>

<div class="wrapper">
  <div class="w-full max-w-xs">

    {#if $state.matches('authenticating')}
      <h2 class="text-2xl text-center">Authenticating ...</h2>
    {/if}

    {#if $state.matches('signedIn')}
      <div class="text-center">
        <button type="button" class="mt-3" on:click={() => send('LOGOUT')}>
          Logout
        </button>
      </div>
    {/if}

    {#if $state.matches('signedOut')}
      <form
        on:submit|preventDefault={loginHandler}
        class="px-8 pt-6 pb-8 bg-white shadow-md"
      >
        <div class="mb-4">
          <label for="email">Email</label>
          <input
            class="input-field"
            id="email"
            type="email"
            placeholder="name@acme.com"
          />
        </div>
        <div class="mb-6">
          <label for="password">Password</label>
          <input
            class="input-field"
            id="password"
            type="password"
            placeholder="******************"
          />
        </div>
        <div>
          <button type="submit">Sign In</button>
        </div>
        <div class="mt-3">
          <button type="button" on:click|preventDefault={() => send('LOGIN')}>
            Sign In with Google
          </button>
        </div>
      </form>
    {/if}

    <!-- debugging: show current state -->
    <div class="p-3 mt-5 bg-blue-200">
      <pre>{JSON.stringify($state.value, null, 2)}</pre>
    </div>
  </div>
</div>
```

You might have noticed that we only have "if" statements in our code. By using XState you can only be in one state at a time. Pretty neat!

## Manipulating app state using XState actions

We've come far, but without app state our app is useless. Let's do something about it.

Remember that we defined a `context` property in our auth machine config? That's where we are going to keep our app state.

For manipulating it, XState has the concept of [actions](https://xstate.js.org/docs/guides/actions.html#declarative-actions), fire-and-forget side effects.

XState actions are functions that take **context** and **event** as arguments. Actions can do many useful things, but you will most likely use actions to manipulate the XState context.

To understand the concept we will continue with a pseudo-example of transitioning from `authenticating` state, but instead we will reject the promise and put that error on the `context.error` property.

```js
import { assign } from 'xstate';

// ...

states: {
  authenticating: {
    invoke: {
      id: 'checkAuth',
      src: (ctx, event) => {
        console.log({ ctx, event });
        return new Promise((resolve, reject) => {
          // simulate a slow auth method
          setTimeout(() => reject('wrong password'), 1000);
        });
      },
      // called on Promise.resolve()
      onDone: {
        target: 'signedIn'
      },
      // called on Promise.reject()
      onError: {
        target: 'signedOut',
        // assign the error to the `context.error`
        // that comes in event.data from the Promise.reject.
        // assign is a helper function that you import from XState
        // that we will use later
        actions: assign({
          error: (_, event) => event.data
        })
      }
    }
  },
  // ...
  signedIn: {},
  signedOut: {}
}
```

XState is flexible when it comes to defining its config. You can supply a list of actions and also pass them in a separate external dictionary and refer to them by key name instead of writing them inline.

Actions are kind of hard to understand, but stick with me. Everything will clear as we progress further.

## Defining side effects as XState services

Now that we know how to perform side effects in XState we need to wrap all Firebase authentication methods in XState services and also add actions to manipulate our app state.

Remember that I told that we can supply both actions and services as external objects and refer to them by name in our config? Let's do that.

We will start with XState services. Open the `auth/index.js` file and replace `initAuth` function with this code.

```js
// auth/index.js

// constructor function. need to call it after we
// initialize our firebase app
export const initAuth = (useRedirect = false) => {
	const auth = firebase.auth();

	const loginWithEmailPassword = (email, password) =>
		auth.signInWithEmailAndPassword(email, password);

	const loginWithGoogle = () => {
		const provider = new firebase.auth.GoogleAuthProvider();

		return useRedirect
			? auth.signInWithRedirect(provider)
			: auth.signInWithPopup(provider);
	};

  // define XState services
  const services = {
    authChecker: () =>
      // wrap the onAuthStateChanged hook in a promise and
      // immediately unsubscribe when triggered
      new Promise((resolve, reject) => {
        const unsubscribe = firebase.auth().onAuthStateChanged(auth => {
          unsubscribe();
          return auth ? resolve(auth) : reject();
        });
      }),
    authenticator: (_, event) => {
      if (event.provider === 'email') {
        return loginWithEmailPassword(event.email, event.password);
      } else if (event.provider === 'google') {
        return loginWithGoogle();
      }
    },
    loader: (ctx, _) => {
      return new Promise(resolve => {
        setTimeout(() => {
          ctx.auth
            .getIdTokenResult()
            .then(({ claims }) => userMapper(claims))
            .then(resolve);
        }, 1500);
      });
    },
    logout: () => auth.signOut()
  };

  // pass the services as argument
	const authMachine = initAuthMachine(services);

	return useMachine(authMachine);
};
```

It's time to fix our auth machine config.

## Fixing the auth machine config

I can talk a lot about how to adjust our config, but code speaks louder than words, even if it's made up of words.

Here is updated `authMachine.js` in all its glory, with comments and all.

```js
// authMachine.js

import { createMachine, assign } from 'xstate';

const config = {
  id: 'auth',
  // we want to start by checking if
  // user is logged in when page loads
  initial: 'authenticating',
  // context is where you keep state
  context: {
    auth: null,
    user: null,
    error: null
  },
  // all possible authentication states
  states: {
    authenticating: {
      // when entering a state invoke
      // the authChecker service
      invoke: {
        id: 'authChecker',
        src: 'authChecker',
        onDone: { target: 'loading', actions: 'setAuth' },
        onError: { target: 'signedOut' }
      }
    },
    // we will enrich the user profile
    // with additional data
    loading: {
      invoke: {
        id: 'loader',
        src: 'loader',
        onDone: { target: 'signedIn', actions: 'setUser' },
        onError: {
          target: 'signedOut.failure',
          actions: ['setError', 'clearAuth']
        }
      }
    },
    signedIn: {
      // when receiving 'LOGOUT' event
      // transition to singingOut state
      on: { LOGOUT: { target: 'signingOut' } }
    },
    // signedOut has two sub-states
    // we will transition to failure in
    // case of wrong password, username
    // or network error
    signedOut: {
      initial: 'ok',
      states: {
        ok: { type: 'final' },
        failure: {}
      },
      on: {
        LOGIN: { target: 'signingIn' }
      }
    },
    signingIn: {
      invoke: {
        id: 'authenticator',
        src: 'authenticator',
        onDone: {
          target: 'authenticating',
          // clear error if successful login
          actions: 'clearError'
        },
        onError: {
          // transition to failure state
          // and set an error
          target: 'signedOut.failure',
          actions: 'setError'
        }
      }
    },
    signingOut: {
      invoke: {
        id: 'logout',
        src: 'logout',
        onDone: {
          target: 'signedOut',
          actions: ['clearAuth', 'clearError']
        },
        onError: {
          target: 'signedOut.failure',
          actions: ['clearAuth', 'setError']
        }
      }
    }
  }
};

export const initAuthMachine = services => {
  // define XState actions so that we can
  // refer to them by name in machine config
  const actions = {
    // clear user info on logout
    clearAuth: assign({ user: null, auth: null }),
    clearError: assign({ error: null }),
    // put Firebase auth object on context
    setAuth: assign({ auth: (_, event) => event.data }),
    // put user on context in loading service
    setUser: assign({ user: (_, event) => event.data }),
    setError: assign({
      error: (_, event) => event.data
    })
  };

  // create an options object containing
  // actions and services
  const options = {
    actions,
    services
  };

  return createMachine(config, options);
};
```

And this is what it looks like in the XState's visualizer.

![XState visualizer Firebase authentication machine](https://res.cloudinary.com/codechips/image/upload/v1596197355/posts/firebase-auth-xstate/xstate-visualizer-firebase-auth-machine_oeq2d5.webp)

If you start at the `authenticating` state, and follow a path, it will all make sense. Notice that in configuration we refer to our services and actions by their names (keys) instead of writing them inline.

One thing to know is that if you have a transition with no action you can omit the `target` keyword and just use a string. XState is flexible, but in my opinion, it's good to be consistent and not take any shortcuts.

You can also see that we supplied an `options` object with our *actions* and *services* to `createMachine` functions. XState has plenty of different options to create your machines.

## Understanding XState events

Just like in Redux, you define an action, but it's called **event** in this case.

In the event you can also supply event data. Here is the [link](https://xstate.js.org/docs/guides/interpretation.html#sending-events) to the XState documentation that explains it in great detail.

Below are a few examples of XState events.

```js
// simplest case possible
send('LOGIN');

// same as above, this is what XState tranforms to internally
send({ type: 'LOGIN'});

// provide an object payload
send('LOGIN', { provider: 'google'});

// or
send({type:'LOGIN', provider: 'google'});

qend('LOGIN', { provider: 'email', email: 'john@example.com', password: 'qwerty' });
```

It's crucial that you understand the concept of XState events, because they are the base of everything in XState.

## Using auth machine states in UI

Empowered by our new knowledge, let's do a final adjustment to our UI in `App.svelte`. We already logic for state management in place. We only need to adjust our handlers with correct events and also take care of error handling.

Here is the final version of `App.svelte` sprinkled with comments.

```html
<!-- App.svelte -->

<style lang="postcss">
	label {
		@apply block mb-2 text-sm font-bold text-gray-700;
	}
	.input-field {
		@apply border w-full py-2 px-3 text-gray-700 mb-3;
	}
	.input-field:focus {
		@apply shadow-outline outline-none;
	}
	button {
		@apply w-full px-4 py-2 font-bold text-white bg-blue-500 rounded-sm;
	}
	button:hover {
		@apply bg-blue-700;
	}
	button:focus {
		@apply outline-none shadow-outline;
	}
	.wrapper {
		@apply flex flex-grow h-screen justify-center items-center bg-blue-100;
	}
</style>

<script>
	import firebase from 'firebase/app';
	import { initAuth } from './auth';
	import { fade } from 'svelte/transition';

	const firebaseConfig = {
		// vite reads env variables from .env file in root dir
		apiKey: `${import.meta.env.VITE_FIREBASE_KEY}`,
		authDomain: 'testing-firebase-emulators.firebaseapp.com',
		projectId: 'testing-firebase-emulators'
	};

	firebase.initializeApp(firebaseConfig);

	// use custom auth machine store
	const { state, send } = initAuth();

	const loginHandler = async event => {
		const { email, password } = event.target.elements;
		// send login event
		send('LOGIN', {
			provider: 'email',
			email: email.value,
			password: password.value
		});
	};

	// we don't want to be explicit about signingIn state
	$: displayLoginForm = ['signingIn', 'signedOut'].some($state.matches);
</script>

<div class="wrapper">
	<div class="w-full max-w-xs">
		{#if $state.matches('authenticating')}
			<h2 class="text-2xl text-center">Authenticating ...</h2>
		{/if}

		{#if $state.matches('loading')}
			<h2 class="text-2xl text-center">Loading ...</h2>
		{/if}

		<!-- uncomment if you want to be explicit about signingIn state -->
		<!--
		{#if $state.matches('signingIn')}
			<h2 class="text-2xl text-center">Signing in ...</h2>
		{/if}
		-->

		{#if $state.matches('signedIn')}
			<div class="text-center">
				<h2 class="text-2xl">{$state.context.user.email}</h2>
				<button type="button" class="mt-3" on:click={() => send('LOGOUT')}>
					Logout
				</button>
			</div>
		{/if}

		{#if displayLoginForm}
			<form
				on:submit|preventDefault={loginHandler}
				class="px-8 pt-6 pb-8 bg-white shadow-md"
			>
				<div class="mb-4">
					<label for="email">Email</label>
					<input
						class="input-field"
						id="email"
						type="email"
						placeholder="name@acme.com"
					/>
				</div>
				<div class="mb-6">
					<label for="password">Password</label>
					<input
						class="input-field"
						id="password"
						type="password"
						placeholder="******************"
					/>
				</div>
				<!-- show auth errors -->
				{#if $state.context.error}
					<div class="bg-red-500 p-3 mb-6" transition:fade>
						{$state.context.error}
					</div>
				{/if}
				<div>
					<button type="submit">Sign In</button>
				</div>
				<div class="mt-3">
					<!-- tell auth machine that this is a Google login -->
					<button
						type="button"
						on:click|preventDefault={() => send('LOGIN', {
								provider: 'google'
							})}
					>
						Sign In with Google
					</button>
				</div>
			</form>
		{/if}

	</div>
</div>

```

If you look carefully you will see that we added data to our events and we also show an error to the user in case of authentication failure.

Notice that we communicate with our machine by sending events. The logic is encapsulated in the machine itself.

I don't know about you, but I actually find it quite beautiful.

While our states are now explicit, I am struggling to show user correct information (or state transitions).

But this is not a code problem, only my lack of UX skills.

To compensate for it I created a reactive variable `displayLoginForm`. That way login form is visible even in the `signingIn` state.

## Conclusion

This was a fun and educating experience. We achieved our goal and have a fully functioning Firebase authentication solution.

There are many ways to structure your application state with XState and if you are not careful you will end up with an unmanageable config that is hard to understand. The key is to write small, single purpose machines.

XState is also flexible in how you define your state machines. Its flexibility is also one of its weaknesses, because it's easy to get lost in all the options available and what to use where. It's easy to get into an analysis paralysis state, especially when you are just starting out learning the library.

With that said, I am happy with results and look forward to exploring XState further. Hope you do too now!

Here is the code to the final solution on Github.
{% github codechips/svelte-firebase-auth-xstate-example no-readme %}

Thanks for reading!