Managing Svelte UI state with Robot FSM
Tags: svelte, webdev, fsm
Published: 2020-11-27 09:18:29

---
title: Managing Svelte UI state with Robot FSM
published: true
date: 2020-10-11 00:00:00 UTC
tags: svelte, webdev,fsm
canonical_url: https://codechips.me/svelte-and-robot-fsm/
---

Ever worked with building complex UIs? If yes, you probably know how challenging it can be to keep track all UI states. It's very easy to end up with a big hairy ball of all the state variables.

If you are building semi-complex UIs Finite State Machines can help you deal with state. Learn what FSMs are and how you can use them with a simple example.

## What is a Finite State Machine?

Shortly, Finite State Machine or FSM is an old, but recently rediscovered pattern, to help us deal with complex state.

The gist of FSM is that your application can only be in one state at a time. If you think about it, it actually makes sense. Just like in real life, you can only be in one state at at time. Unless you are a quark.

I find Finite State Machines a great fit when working with complex UIs. Applications are getting more and more interactive and keeping track of UI state can quickly get challenging.

## What is Robot FSM?

There are currently two popular FSM libraries in JavaScript that I know of. [XState](https://xstate.js.org/docs/) and [Robot](https://thisrobot.life/).

I would say that XState is the most popular. It has a lot of features and really great documentation. However, FSM can be intimidating to learn and _XState_ especially, because there you can achieve the same result by different ways.

If you are interested in XState, I've written an article where I used XState as a state machine for Firebase Authentication - [Firebase authentication with XState and Svelte](https://codechips.me/firebase-authentication-with-xstate-and-svelte/).

For this example I decided to use [Robot FSM library](https://thisrobot.life/). First, it's much smaller than _XState_ - 1kb vs 13kb. Second, it's also easier to learn, because it has less bells and whistles.

Robot doesn't have as good documentation as _XState_, so you have to read between the lines and experiment a bit to understand it's concepts and how to work with it.

But that's why I am here! Hopefully by the end of this article everything will click, you will know what an FSM is and understand where you can use them.

I promise to keep things simple. Let's code!

## Setting up project

First, we need to create a new Svelte project. We will use [Svite](https://github.com/dominikg/svite) with [pnpm](https://pnpm.js.org/) package manager to save some disk space. I find this combination the fastest way to get going.

```text
$ npx svite@beta create -pm pnpm svelte-robot-fsm-example
```

We will be building a panel menu with different sections. Think tabs component, only prettier.

Next step is to create a few Svelte components.

## Creating panel components

We will create a simple panel component called `Section.svelte`. It will take a `title` and a `close` function handler as parameters.

Close handler will be wired up to the _Close_ button in the component. When clicked, it should hide the menu. We will also add Svelte's slide transition for some neat visual FX.

```html
<!-- src/components/Section.svelte -->

<script>
  import { slide } from 'svelte/transition';
  export let title = '';
  export let close = () => {};
</script>

<style>
  .section {
    border-bottom: 1px solid #65b6ca;
  }
  .inner {
    position: relative;
    padding: 1em;
    margin: 0 auto;
    max-width: 100ch;
    min-height: 200px;
  }
  h2 {
    font-weight: 700;
    font-size: 3em;
  }
  .close {
    position: absolute;
    bottom: 2em;
  }
</style>

<div class="section" transition:slide>
  <div class="inner">
    <h2>{title}</h2>
    <button on:click={close} class="close">Close</button>
  </div>
</div>
```

Let's setup three sections in our `App.svelte`. Replace the file with the code below.

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

<script>
  import Section from './components/Section.svelte';
</script>

<style>
  .wrapper {
    font-family: sans-serif;
  }
  .container {
    margin: 1em auto;
    max-width: 100ch;
  }
  .panel {
    background-color: #b9f0fe;
  }
  .buttons > button {
    padding: 0.5em 1em;
  }
</style>

<div class="wrapper">
  <div class="heading container">
    <h1>Svelte with Robot FSM Example</h1>
  </div>

  <div class="panel">
    <Section title="One" />
    <Section title="Two" />
    <Section title="Three" />
  </div>

  <div class="buttons container">
    <button>One</button>
    <button>Two</button>
    <button>Three</button>
  </div>
</div>
```

If you start the app (pnpm run dev), all of the three sections should be visible. That's not something we want. Let's add some interactivity logic next.

## Doing it the normal way

Adding interactivity to these panel sections is straight forward. For that we need to add three boolean variables, one for each section, and then manipulate their state.

Here is the updated `App.svelte` file with state variables and `on:click` handlers.

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

<script>
  import Section from './components/Section.svelte';

  // open/close state variables. one for each section.
  let showOne = false;
  let showTwo = false;
  let showThree = false;
</script>

<style>
  .wrapper {
    font-family: sans-serif;
  }
  .container {
    margin: 1em auto;
    max-width: 100ch;
  }
  .panel {
    background-color: #b9f0fe;
  }
  .buttons > button {
    padding: 0.5em 1em;
  }
</style>

<div class="wrapper">
  <div class="heading container">
    <h1>Svelte with Robot FSM Example</h1>
  </div>

  <div class="panel">
    {#if showOne}
      <Section title="One" close={() => (showOne = false)} />
    {/if}

    {#if showTwo}
      <Section title="Two" close={() => (showTwo = false)} />
    {/if}

    {#if showThree}
      <Section title="Three" close={() => (showThree = false)} />
    {/if}
  </div>

  <div class="buttons container">
    <button on:click={() => (showOne = true)}>One</button>
    <button on:click={() => (showTwo = true)}>Two</button>
    <button on:click={() => (showThree = true)}>Three</button>
  </div>
</div>
```

You can see that we added basic `on:click` handlers to buttons and we also pass in `close` handlers that set state variable to `false`.

If you've done everything right you should now be able to open and close each panel. Good times!

![State based panel](https://res.cloudinary.com/codechips/image/upload/v1602447536/posts/svelte-robot-fsm/svelte-robot-fsm-v1_sakxtg.gif)

But what about the scenario that only one section should be visible at one time? Meaning if you open a new one, the currently visible section should close.

It's doable, but things can get pretty hairy, because you have to track individual state of every section. That's something Finite State Machines can help us with.

## Let's bring in the Robot

Let's add Robot FSM library to our project.

```text
$ pnpm add robot3
```

Now we need to create our Finite State Machine. We will name it `panelMachine` as it will be responsible for keeping track of our panel's state.

Add this code to `App.svelte` and I will explain it in a second.

```html
<!-- App.svelte -->
<script>
	// ...
  import { createMachine, state, transition } from 'robot3';

  const panelMachine = createMachine({
    closed: state(
      transition('showOne', 'one'),
      transition('showTwo', 'two'),
      transition('showThree', 'three')
    ),
    one: state(transition('close', 'closed')),
    two: state(transition('close', 'closed')),
    three: state(transition('close', 'closed')),
  });

</script>
```

As you can see we used Robot's `createMachine` function to define a state machine. The machine consists of total four states - _closed_, _one_, _two_ and _three_. Our first state is _closed_ and that will be the state machine will start with. If you look carefully you can see that our state machine is just a simple dictionary.

Each dictionary value is then defined by a `state` function. The [state function](https://thisrobot.life/api/state.html) is used to define state in Robot FSM library. But what going on inside the state functions?

State function is the meat of the Robot library. It's in there that you define your state and transitions to other states. Transitions? States? What?!

Yes, I understand if you are confused and I promised to keep things simple for that reason. Let me explain.

As I wrote earlier, an state machine can only be in one state at at time. When our panel FSM is started it starts in the `closed` state, because it's first in the dictionary.

If you imagine that you are in the `closed` state you can see that inside it we have defined three `transition` functions. Transition [function](https://thisrobot.life/api/transition.html) dictates the states you can go to from the state FSM that is currently in. They are used to move from one state to another.

Every transition function takes a _trigger_ event as first argument and the _destination_ state as second argument. You can name events however you like, but I like to prefix mine with verbs as I like to think of them not as events, but rather commands or actions. By sending an event to the state machine you command it to go to another state than it's currently in.

One important concept to understand is that you can only move those states that you have setup transitions for. So, from our `closed` state we can only go to states `one`, `two` or `three`. If you send in some other event name than `showOne`, `showTwo` or `showThree` our FSM will simply ignore them and just stay in the current state it is in.

Similarly, you can only go to `closed` state from other states by sending a `close` event to our state machine. Are you with me so far?

## Interpreting our state machine

Alright, we have defined our state machine. Now we have to activate it by using `interpret` function from the Robot library.
The `interpret` function takes a machine and wraps it into a service that can send events into the machine to change its states. A service does not mutate a machine. Instead it creates derived machines with the current state set.

You can find the current state of the machine by the `machine.current` property. If you wrap it in the service it will be located under `service.machine.current`.

Here is the code that demonstrates how a machine service works.

```js
// Wrap machine into a service. Every time state changes it's printed.
// A service has a `send` command that we can use to send it events.
const service = interpret(panelMachine, service => {
  console.log('current state: ', service.machine.current);
});

console.log('current state: ', s.machine.current)
// current state: closed

// Transition from `closed` state to `one` [legal]
service.send('showOne');
// current state: one

// Transition from `one` to `two` [illegal]
service.send('showTwo');
// current state: one

// Send non-existent event
service.send('foo');
// current state: one

// Transition from `one` to `closed` [legal]
service.send('close');
// current state: closed
```

As you can see from the example above, if we send in a valid event, that we defined in our state transition in the current state we are in, we can switch to a different state in our state machine.

## Wrapping our state machine service in a Svelte store

Robot does not have Svelte support per default, but it's not hard to wire it up. For that we will can write a custom Svelte store.

```js
import { createMachine, state, transition, interpret} from 'robot3';
import { writable } from 'svelte/store';

export const useMachine = machine => {
	const { subscribe, set } = writable({
		state: machine.current,
		context: machine.context,
	});

	const service = interpret(machine, service => {
		set({ state: service.machine.current, context: service.context });
	});

	return [{ subscribe }, service.send];
};
```

As you can see we defined a `useMachine` constructor function. It takes a Robot FSM as its argument and returns an array.  First value is our custom Svelte store with our machine's `context` and `state` as two properties. Second value is the service `send` method. It will allow us to send events to our machine.

Let's use it in our code.

```js
const [panelState, send] = useMachine(panelMachine);
```

## Using state machine

We have everything we need. Next step we need to do is to replace our `on:click` and `close` handlers. We also need to use our new `panelService` store for state checking instead of individual state variables.

I won't bore you with all the code changes needed. Instead, here is the updated `App.svelte` file in all its glory.

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

<script>
  import Section from './components/Section.svelte';
  import { createMachine, state, transition, interpret } from 'robot3';
  import { writable } from 'svelte/store';

  // constructor function to interpret the machine and wrap
  // the FSM service into a custom svelte store.
  export const useMachine = (machine) => {
    const { subscribe, set } = writable({
      state: machine.current,
      context: machine.context,
    });

    // every time the state changes we will update our Svelte store
    const service = interpret(machine, (service) => {
      set({ state: service.machine.current, context: service.context });
    });

    return [{ subscribe }, service.send];
  };

  // create our Robot FSM definition
  const panelMachine = createMachine({
    closed: state(
      transition('showOne', 'one'),
      transition('showTwo', 'two'),
      transition('showThree', 'three')
    ),
    one: state(transition('close', 'closed')),
    two: state(transition('close', 'closed')),
    three: state(transition('close', 'closed')),
  });

  const [panelState, send] = useMachine(panelMachine);

  // close handler
  const close = () => send('close');

	// send in event to our FSM
  const toggle = (action) => send(action);
</script>

<style>
  .wrapper {
    font-family: sans-serif;
  }
  .container {
    margin: 1em auto;
    max-width: 100ch;
  }
  .panel {
    background-color: #b9f0fe;
  }
  .buttons > button {
    padding: 0.5em 1em;
  }
</style>

<div class="wrapper">
  <div class="heading container">
    <h1>Svelte with Robot FSM Example</h1>
  </div>

  <div class="panel">
    {#if $panelState.state === 'one'}
      <Section title="One" {close} />
    {/if}

    {#if $panelState.state === 'two'}
      <Section title="Two" {close} />
    {/if}

    {#if $panelState.state === 'three'}
      <Section title="Three" {close} />
    {/if}
  </div>

  <div class="buttons container">
    <button on:click={() => toggle('showOne')}>One</button>
    <button on:click={() => toggle('showTwo')}>Two</button>
    <button on:click={() => toggle('showThree')}>Three</button>
  </div>
</div>
```

You can see that we have replaced our individual state variables with our state machine. We have also create two handlers - `close` and `toggle`.

If you start the app now, you will notice that we can only open one panel at a time. But I must say, it's not a correct behaviour. We cannot open other panel before we close the currently open one.

![Robot FSM based panel](https://res.cloudinary.com/codechips/image/upload/v1602447536/posts/svelte-robot-fsm/svelte-robot-fsm-v2_jxmogo.gif)

Why? If you look at our machine definition you can see that the only valid transition from a panel is to the `closed` state. This means the only valid event we can sent to the open panel is `close` event.

We can actually use this knowledge to our advantage by changing our `toggle` handler to the following.

```js
const toggle = (action) => {
	send('close');
	send(action);
};
```

Before sending our show event, we first send the `close` event so that our machine enters `closed` state.

![Robot FSM based panel with auto-switching](https://res.cloudinary.com/codechips/image/upload/v1602447536/posts/svelte-robot-fsm/svelte-robot-fsm-v3_vtrmdg.gif)

I don't know about you, but I find it pretty elegant.

## Implementing toggle functionality

But what if we want our button not only to open, but also toggle a panel's state. Meaning if the panel is currently open clicking the same button should close it.

For that we need add a guard that checks if the current state our machine matches the desired state. If it does, it just closes the panel.

```js
const toggle = (action, state) => {
	// if current state matches desired panel state, close it
	if ($panelState.state === state) {
		send('close');
		return;
	}
	// else first close, then open desired state
	send('close');
	send(action);
};
```

We also need to provide the desired state in our buttons' `on:click` handlers.

```html
<div class="buttons container">
	<button on:click={() => toggle('showOne', 'one')}>One</button>
	<button on:click={() => toggle('showTwo', 'two')}>Two</button>
	<button on:click={() => toggle('showThree', 'three')}>Three</button>
</div>
```

Let's take a look if the toggle works.

![Robot FSM based panel with toggle](https://res.cloudinary.com/codechips/image/upload/v1602448022/posts/svelte-robot-fsm/svelte-robot-fsm-v4_errgpl.gif)

Yep. Works dandy fine! Mission accomplished.

## Conclusion

Finite State Machines are hard to understand if you haven't been exposed to them before. They also offer so much more than I showed in my example.

In Robot there is also _context_ that lets you work with state, _guards_ that allows you to enter a state only if condition is met, _invoke_ that helps us work with promises and sub-machines, and _immediate_ transitions.

XState has even more to offer. It's easy to get lost if you don't have a solid understanding of the basics first.

I chose the simplest example I could think of on purpose that only shows how to setup state and transition from one state to another by sending events.

If you want to test you skills try building a tabs component. The concepts are exactly the same and you can re-use a lot of the code we have written.

When you understand the concepts and how to work with state, all other FSM concepts become easy. In the end, it's all about states, sending events and transitions.

Start simple, experiment and I can guarantee that you will quickly start seeing places in your codebase where a state machine makes sense to use. Plus, as a bonus, you will get one more powerful tool in your toolbox.

You can find the full code at https://github.com/codechips/svelte-robot-fsm-example

Thanks for reading! Now go and replace something in your codebase with an FSM FTW.