Skip to content

State Machines using XState and Svelte (Part 1)

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

State Machines using XState and Svelte (Part 1)

In this blog post, we'll learn about state machines, and how to implement them in Svelte with XState.

What's a State Machine

  • A state machine is a representation- an abstraction of the behavior of a system.
  • A Finite State Machine (FSM) is a State machine that can have only one state at a given time, and has a finite number of states.
  • Other properties for FSMs are having an initial state, and a finite number of events.
  • Given a state and an event, a transition function will determine what the next state is.

What's XState

XState is a library that will allow us to create and interpret FSMs and statecharts. Its core is framework agnostic, and there are utilities for many of them.

Our first state machine with Svelte

We'll start from a basic component, add features to it, and then we'll create a state machine that will have the same behavior. Let's create a new project.

npm init vite@latest

✔ Project name: · xstate-svelte
✔ Select a framework: · svelte
✔ Select a variant: · svelte-ts

cd xstate-svelte
npm install //use the package manager you prefer
npm run dev

Our project contains a Counter component that looks like the one below. We'll start from here to model our state machine later.

<!-- Counter.svelte -->
<script lang="ts">
  let count: number = 0
  const increment = () => {
    count += 1
  }
</script>

<button on:click={increment}>
  Clicks: {count}
</button>

<style>
  button {
    font-family: inherit;
    font-size: inherit;
    padding: 1em 2em;
    color: #ff3e00;
    background-color: rgba(255, 62, 0, 0.1);
    border-radius: 2em;
    border: 2px solid rgba(255, 62, 0, 0);
    outline: none;
    width: 200px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
  }

  button:focus {
    border: 2px solid #ff3e00;
  }

  button:active {
    background-color: rgba(255, 62, 0, 0.2);
  }
</style>

Our component is a button that can increment a count when clicking it. Let's add some functionality to it like adding the possibility to decrement the count as well.

<!-- Counter.svelte -->
<script lang="ts">
  let count: number = 0
  const increment = () => {
    count += 1
  }
  const decrement = () => {
    count -= 1
  }
</script>

<p>Count: {count}</p>
<button on:click={increment}>
  Increment
</button>
<button on:click={decrement}>
  Decrement
</button>

<!-- ... -->
xstate01

Our component is working, but let's say we would like to avoid reaching a count below 0 or above 10, we would have to add a guard to avoid calling the increment or decrement functions when the minimum or maximum values are reached.

<!-- Counter.svelte -->
<!-- ... -->
<button
  on:click={() => {
    if (count < 10) {
      increment();
    }
  }}
>
  Increment
</button>
<button
  on:click={() => {
    if (count > 0) {
      decrement();
    }
  }}
>

<!-- ... -->
xstate2

Finally, let's add a button to turn the counter on and off, and enable or disable changing the count.

<!-- Counter.svelte -->
<script lang="ts">
  let count: number = 0;
  let active: boolean = true
  const increment = () => {
    count += 1;
  };
  const decrement = () => {
    count -= 1;
  };
  const toggleActive = () => {
    active = !active
  };
</script>
<p>The counter is {active ? "enabled": "disabled"}</p>
<p>Count: {count}</p>
<button
  on:click={() => {
    if (active && count < 10) {
      increment();
    }
  }}
>
  Increment
</button>
<button
  on:click={() => {
    if (active && count > 0) {
      decrement();
    }
  }}
>
  Decrement
</button>

<button
  on:click={toggleActive}
>
  On/Off
</button>

<!-- ... -->
xstate3

Now, it's finally time to recreate this same component but using XState.

First, we'll need to install the required dependencies.

npm i xstate @xstate/svelte --save

Before we dive into the code, let's think about our state machine. If we could define the properties of it, it would look something like this.

  • possible states: enabled - disabled
  • possible events: increment-decrement - enable/disable
  • initial state: enabled
  • guards: max value, min value
  • context: in this case, the count value

If we start expressing this as an object, we could do something like:

{
	// initial state,
	initial: enabled 
	// possible states (StateNodes)
	states:{
		enabled: {
		},
		disabled: {
		}
	},
}

Each key inside our states object will represent a StateNode with its configuration. To add the events, we will use an object too, and they can be declared inside of a specific state, or at the root of the configuration object if it's global. In our example:

{
	initial: enabled 
	states:{
		enabled: {
			on: {
				increment: { // do something },
				decrement: { // do something },
				toggle: { // do something },
			}
		},
		disabled: {
			toggle: { // do something },
		}
	},
}

XState initializes a state machine with an object similar to this. We will change our Counter component to implement it. Starting from the original component to the final result.

<!-- Counter.svelte -->
<script lang="ts">
  import { createMachine, assign } from 'xstate';
  import { useMachine } from '@xstate/svelte';

  const enum States {
    Enabled = 'Enabled',
  }

  const enum Events {
    Increment = 'Increment',
  }

  const increment = (ctx) => ctx.count + 1;

  const counterMachine = createMachine({
    initial: States.Enabled,
    context: {
      count: 0,
    },
    states: {
      [States.Enabled]: {
        on: {
          [Events.Increment]: {
            actions: assign({ count: increment }),
          },
        },
      },
    },
  });

  const { state, send } = useMachine(counterMachine);
</script>

<button on:click={() => send(Events.Increment)}>
  Clicks: {$state.context.count}
</button>

First, we define the states, and events that our state machines will have and respond to:

Our initial counter had only one state, and one possible event.

Then, we define a function that will respond to that event, taking the state machine context, and adding 1 to the context count.

Next, we use the @xstate/svelte method useMachine that will return the state of the machine, and a method to send events to it.

Note that state is a Svelte store that you can subscribe to, to get the current state of the machine using the $ prefix.

In this example, $state.context.count is updated each time Events.Increment is sent.

To add the decrement functionality, we just need to add a new event and the corresponding handler.

<!-- Counter.svelte -->
<script lang="ts">
  // ...

 const enum Events {
    Increment = 'Increment',
    Decrement = 'Decrement',
  }

  const increment = (ctx) => ctx.count + 1;
  const decrement = (ctx) => ctx.count - 1;

  const counterMachine = createMachine({
    initial: States.Enabled,
    context: {
      count: 0,
    },
    states: {
      [States.Enabled]: {
        on: {
          [Events.Increment]: {
            actions: assign({ count: increment }),
          },
          [Events.Decrement]: {
            actions: assign({ count: decrement }),
          },
        },
      },
    },
  });

  const { state, send } = useMachine(counterMachine);
</script>

<p>Count: {$state.context.count}</p>
<button on:click={() => send(Events.Increment)}>
  Increment
</button>
<button on:click={() => send(Events.Decrement)}>
  Decrement
</button>

To add guards to our events, we will need to set the cond property of the event handlers. If the conditions are met, then the actions are triggered.

<!-- Counter.svelte -->
<script lang="ts">
  // ...

  const counterMachine = createMachine({
    initial: States.Enabled,
    context: {
      count: 0,
    },
    states: {
      [States.Enabled]: {
        on: {
          [Events.Increment]: {
            actions: assign({ count: increment }),
            cond: (ctx) => ctx.count < 10,
          },
          [Events.Decrement]: {
            actions: assign({ count: decrement }),
            cond: (ctx) => ctx.count > 0,
          },
        },
      },
    },
  });

  const { state, send } = useMachine(counterMachine);
</script>

<!-- ... -->

One last part, and we are done. We are missing the enable/disable button. In this case, we will add an event and a new state.

<!-- Counter.svelte -->
<script lang="ts">
  // ...

  const enum States {
    Enabled = 'Enabled',
    Disabled = 'Disabled',
  }

  const enum Events {
    Increment = 'Increment',
    Decrement = 'Decrement',
    ToggleEnabled = 'ToggleEnabled',
  }

  const increment = (ctx) => ctx.count + 1;
  const decrement = (ctx) => ctx.count - 1;

  // Set state machine
  const counterMachine = createMachine({
    initial: States.Enabled,
    context: {
      count: 0,
    },
    states: {
      [States.Enabled]: {
        on: {
          [Events.Increment]: {
            actions: assign({ count: increment }),
            cond: (ctx) => ctx.count < 10,
          },
          [Events.Decrement]: {
            actions: assign({ count: decrement }),
            cond: (ctx) => ctx.count > 0,
          },
          [Events.ToggleEnabled]: States.Disabled
        },
      },
      [States.Disabled]: {
        on: {
          [Events.ToggleEnabled]: States.Enabled
        },
      },
    },
  });

  const { state, send } = useMachine(counterMachine);
</script>

<p>The counter is {$state.value === States.Enabled ? "enabled": "disabled"}</p>
<p>Count: {$state.context.count}</p>
<button on:click={() => send(Events.Increment)}> Increment </button>
<button on:click={() => send(Events.Decrement)}> Decrement </button>
<button on:click={() =

<!-- ... -->

We are now defining a new state Disabled that will only handle the toggle event that will change the state to Enabled. We do the opposite when the counter is on.

Let's focus on this line of the previous example

[Events.ToggleEnabled]: States.Enabled

The response to this event looks a lot different than the previous ones. In the example above, we're using the shorthand value when you only need to transition from one state to another.

It's equivalent to:

[Events.ToggleEnabled]: {
  target: States.Enabled,
}

The transition configuration object has the following signature:

interface TransitionConfig<TContext', TEvent'> {
cond?: Condition<TContext', TEvent'> // define a guard to this transition
actions?: Actions<TContext', TEvent'> // what actions to perform
in?: StateValue
internal?: boolean
target?: TransitionTarget<TContext', TEvent'> // the next State
meta?: Record<string, any>
description?: string
}

Statecharts

Statecharts are a visual representation of the states of a process.

The best part is that we don't need to change our code to see it in action.

Go to https://stately.ai/viz, and paste this part of the code.

import { createMachine, assign } from 'xstate';

const enum States {
    Enabled = 'Enabled',
    Disabled = 'Disabled',
  }

  const enum Events {
    Increment = 'Increment',
    Decrement = 'Decrement',
    ToggleEnabled = 'ToggleEnabled',
  }

  const increment = (ctx) => ctx.count + 1;
  const decrement = (ctx) => ctx.count - 1;

  // Set state machine
  const counterMachine = createMachine({
    initial: States.Enabled,
    context: {
      count: 0,
    },
    states: {
      [States.Enabled]: {
        on: {
          [Events.Increment]: {
            actions: assign({ count: increment }),
            cond: (ctx) => ctx.count < 10,
          },
          [Events.Decrement]: {
            actions: assign({ count: decrement }),
            cond: (ctx) => ctx.count > 0,
          },
          [Events.ToggleEnabled]: States.Disabled
        },
      },
      [States.Disabled]: {
        on: {
          [Events.ToggleEnabled]: States.Enabled
        },
      },
    },
  });

Next, click the Visualize button, and enjoy your statechart.

xstate4

We can see that the toggleEnabled event changes from enabled to disabled and vice-versa. At the same time, the other events are available or not, depending on the current state (represented in light blue when available).

This is a nice way of visualizing our state machine, you could even start from here, and then move to your favorite framework. It may help spot issues and challenges early.

What's Next

In this intro to XState and Svelte, we learned how to transform a simple component to use state machines. In the next part of this series, we'll create a more complex app, and we'll explore the API in depth.

You can find the code from this tutorial in this repo.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Incremental Hydration in Angular cover image

Incremental Hydration in Angular

Incremental Hydration in Angular Some time ago, I wrote a post about SSR finally becoming a first-class citizen in Angular. It turns out that the Angular team really treats SSR as a priority, and they have been working tirelessly to make SSR even better. As the previous blog post mentioned, full-page hydration was launched in Angular 16 and made stable in Angular 17, providing a great way to improve your Core Web Vitals. Another feature aimed to help you improve your INP and other Core Web Vitals was introduced in Angular 17: deferrable views. Using the @defer blocks allows you to reduce the initial bundle size and defer the loading of heavy components based on certain triggers, such as the section entering the viewport. Then, in September 2024, the smart folks at Angular figured out that they could build upon those two features, allowing you to mark parts of your application to be server-rendered dehydrated and then hydrate them incrementally when needed - hence incremental hydration. I’m sure you know what hydration is. In short, the server sends fully formed HTML to the client, ensuring that the user sees meaningful content as quickly as possible and once JavaScript is loaded on the client side, the framework will reconcile the rendered DOM with component logic, event handlers, and state - effectively hydrating the server-rendered content. But what exactly does "dehydrated" mean, you might ask? Here's what will happen when you mark a part of your application to be incrementally hydrated: 1. Server-Side Rendering (SSR): The content marked for incremental hydration is rendered on the server. 2. Skipped During Client-Side Bootstrapping: The dehydrated content is not initially hydrated or bootstrapped on the client, reducing initial load time. 3. Dehydrated State: The code for the dehydrated components is excluded from the initial client-side bundle, optimizing performance. 4. Hydration Triggers: The application listens for specified hydration conditions (e.g., on interaction, on viewport), defined with a hydrate trigger in the @defer block. 5. On-Demand Hydration: Once the hydration conditions are met, Angular downloads the necessary code and hydrates the components, allowing them to become interactive without layout shifts. How to Use Incremental Hydration Thanks to Mark Thompson, who recently hosted a feature showcase on incremental hydration, we can show some code. The first step is to enable incremental hydration in your Angular application's appConfig using the provideClientHydration provider function: ` Then, you can mark the components you want to be incrementally hydrated using the @defer block with a hydrate trigger: ` And that's it! You now have a component that will be server-rendered dehydrated and hydrated incrementally when it becomes visible to the user. But what if you want to hydrate the component on interaction or some other trigger? Or maybe you don't want to hydrate the component at all? The same triggers already supported in @defer blocks are available for hydration: - idle: Hydrate once the browser reaches an idle state. - viewport: Hydrate once the component enters the viewport. - interaction: Hydrate once the user interacts with the component through click or keydown triggers. - hover: Hydrate once the user hovers over the component. - immediate: Hydrate immediately when the component is rendered. - timer: Hydrate after a specified time delay. - when: Hydrate when a provided conditional expression is met. And on top of that, there's a new trigger available for hydration: - never: When used, the component will remain static and not hydrated. The never trigger is handy when you want to exclude a component from hydration altogether, making it a completely static part of the page. Personally, I'm very excited about this feature and can't wait to try it out. How about you?...

Understanding Sourcemaps: From Development to Production cover image

Understanding Sourcemaps: From Development to Production

What Are Sourcemaps? Modern web development involves transforming your source code before deploying it. We minify JavaScript to reduce file sizes, bundle multiple files together, transpile TypeScript to JavaScript, and convert modern syntax into browser-compatible code. These optimizations are essential for performance, but they create a significant problem: the code running in production does not look like the original code you wrote. Here's a simple example. Your original code might look like this: ` After minification, it becomes something like this: ` Now imagine trying to debug an error in that minified code. Which line threw the exception? What was the value of variable d? This is where sourcemaps come in. A sourcemap is a JSON file that contains a mapping between your transformed code and your original source files. When you open browser DevTools, the browser reads these mappings and reconstructs your original code, allowing you to debug with variable names, comments, and proper formatting intact. How Sourcemaps Work When you build your application with tools like Webpack, Vite, or Rollup, they can generate sourcemap files alongside your production bundles. A minified file references its sourcemap using a special comment at the end: ` The sourcemap file itself contains a JSON structure with several key fields: ` The mappings field uses an encoding format called VLQ (Variable Length Quantity) to map each position in the minified code back to its original location. The browser's DevTools use this information to show you the original code while you're debugging. Types of Sourcemaps Build tools support several variations of sourcemaps, each with different trade-offs: Inline sourcemaps: The entire mapping is embedded directly in your JavaScript file as a base64 encoded data URL. This increases file size significantly but simplifies deployment during development. ` External sourcemaps: A separate .map file that's referenced by the JavaScript bundle. This is the most common approach, as it keeps your production bundles lean since sourcemaps are only downloaded when DevTools is open. Hidden sourcemaps: External sourcemap files without any reference in the JavaScript bundle. These are useful when you want sourcemaps available for error tracking services like Sentry, but don't want to expose them to end users. Why Sourcemaps During development, sourcemaps are absolutely critical. They will help avoid having to guess where errors occur, making debugging much easier. Most modern build tools enable sourcemaps by default in development mode. Sourcemaps in Production Should you ship sourcemaps to production? It depends. While security by making your code more difficult to read is not real security, there's a legitimate argument that exposing your source code makes it easier for attackers to understand your application's internals. Sourcemaps can reveal internal API endpoints and routing logic, business logic, and algorithmic implementations, code comments that might contain developer notes or TODO items. Anyone with basic developer tools can reconstruct your entire codebase when sourcemaps are publicly accessible. While the Apple leak contained no credentials or secrets, it did expose their component architecture and implementation patterns. Additionally, code comments can inadvertently contain internal URLs, developer names, or company-specific information that could potentially be exploited by attackers. But that’s not all of it. On the other hand, services like Sentry can provide much more actionable error reports when they have access to sourcemaps. So you can understand exactly where errors happened. If a customer reports an issue, being able to see the actual error with proper context makes diagnosis significantly faster. If your security depends on keeping your frontend code secret, you have bigger problems. Any determined attacker can reverse engineer minified JavaScript. It just takes more time. Sourcemaps are only downloaded when DevTools is open, so shipping them to production doesn't affect load times or performance for end users. How to manage sourcemaps in production You don't have to choose between no sourcemaps and publicly accessible ones. For example, you can restrict access to sourcemaps with server configuration. You can make .map accessible from specific IP addresses. Additionally, tools like Sentry allow you to upload sourcemaps during your build process without making them publicly accessible. Then configure your build to generate sourcemaps without the reference comment, or use hidden sourcemaps. Sentry gets the mapping information it needs, but end users can't access the files. Learning from Apple's Incident Apple's sourcemap incident is a valuable reminder that even the largest tech companies can make deployment oversights. But it also highlights something important: the presence of sourcemaps wasn't actually a security vulnerability. This can be achieved by following good security practices. Never include sensitive data in client code. Developers got an interesting look at how Apple structures its Svelte codebase. The lesson is that you must be intentional about your deployment configuration. If you're going to include sourcemaps in production, make that decision deliberately after considering the trade-offs. And if you decide against using public sourcemaps, verify that your build process actually removes them. In this case, the public repo was quickly removed after Apple filed a DMCA takedown. (https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md) Making the Right Choice So what should you do with sourcemaps in your projects? For development: Always enable them. Use fast options, such as eval-source-map in Webpack or the default configuration in Vite. The debugging benefits far outweigh any downsides. For production: Consider your specific situation. But most importantly, make sure your sourcemaps don't accidentally expose secrets. Review your build output, check for hardcoded credentials, and ensure sensitive configurations stay on the backend where they belong. Conclusion Sourcemaps are powerful development tools that bridge the gap between the optimized code your users download and the readable code you write. They're essential for debugging and make error tracking more effective. The question of whether to include them in production doesn't have a unique answer. Whatever you decide, make it a deliberate choice. Review your build configuration. Verify that sourcemaps are handled the way you expect. And remember that proper frontend security doesn't come from hiding your code. Useful Resources * Source map specification - https://tc39.es/ecma426/ * What are sourcemaps - https://web.dev/articles/source-maps * VLQ implementation - https://github.com/Rich-Harris/vlq * Sentry sourcemaps - https://docs.sentry.io/platforms/javascript/sourcemaps/ * Apple DMCA takedown - https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md...

The Future of Dates in JavaScript: Introducing Temporal cover image

The Future of Dates in JavaScript: Introducing Temporal

The Future of Dates in JavaScript: Introducing Temporal What is Temporaal? Temporal is a proposal currently at stage 3 of the TC39 process. It's expected to revolutionize how we handle dates in JavaScript, which has always been a challenging aspect of the language. But what does it mean that it's at stage 3 of the process? * The specification is complete * It has been reviewed * It's unlikely to change significantly at this point Key Features of Temporal Temporal introduces a new global object with a fresh API. Here are some important things to know about Temporal: 1. All Temporal objects are immutable 2. They're represented in local calendar systems, but can be converted 3. Time values use 24-hour clocks 4. Leap seconds aren't represented Why Do We Need Temporal? The current Date object in JavaScript has several limitations: * No support for time zones other than the user's local time and UTC * Date objects can be mutated * Unpredictable behavior * No support for calendars other than Gregorian * Daylight savings time issues While some of these have workarounds, not all can be fixed with the current Date implementation. Let's see some useful examples where Temporal will improve our lives: Some Examples Creating a day without a time zone is impossible using Date, it also adds time beyond the date. Temporal introduces PlainDate to overcome this. ` But what if we want to add timezone information? Then we have ZonedDateTime for this purpose. The timezone must be added in this case, as it also allows a lot of flexibility when creating dates. ` Temporal is very useful when manipulating and displaying the dates in different time zones. ` Let's try some more things that are currently difficult or lead to unexpected behavior using the Date object. Operations like adding days or minutes can lead to inconsistent results. However, Temporal makes these operations easier and consistent. ` Another interesting feature of Temporal is the concept of Duration, which is the difference between two time points. We can use these durations, along with dates, for arithmetic operations involving dates and times. Note that Durations are serialized using the ISO 8601 duration format ` Temporal Objects We've already seen some of the objects that Temporal exposes. Here's a more comprehensive list. * Temporal * Temporal.Duration` * Temporal.Instant * Temporal.Now * Temporal.PlainDate * Temporal.PlainDateTime * Temporal.PlainMonthDay * Temporal.PlainTime * Temporal.PlainYearMonth * Temporal.ZonedDateTime Try Temporal Today If you want to test Temporal now, there's a polyfill available. You can install it using: ` Note that this doesn't install a global Temporal object as expected in the final release, but it provides most of the Temporal implementation for testing purposes. Conclusion Working with dates in JavaScript has always been a bit of a mess. Between weird quirks in the Date object, juggling time zones, and trying to do simple things like “add a day,” it’s way too easy to introduce bugs. Temporal is finally fixing that. It gives us a clear, consistent, and powerful way to work with dates and times. If you’ve ever struggled with JavaScript dates (and who hasn’t?), Temporal is definitely worth checking out....

This Dot AI Field Notes - Anatomy of a Coding Harness cover image

This Dot AI Field Notes - Anatomy of a Coding Harness

A coding agent is not magic, it’s a loop. We call this a harness. The harness is a deterministic layer of code that wraps an LLM. Claude Code is a harness. Codex is a harness. Pi is a harness. The harness, on initialization, provides to the LLM a system prompt defining all tools the harness implements for the LLM. Without the harness, you cannot read or modify files on the user’s local filesystem without them having to copy-and-pasting by hand. The harness is the final place where engineers can customize how coding agents do work before the LLM takes over. Think of the LLM as a train and the harness as the rails the train rides on. Below… one full task executed by a harness, traced step by step....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co