XState

repository·main·Indexed 12 days ago

https://github.com/statelyai/xstate

An actor-based state management and orchestration solution for JavaScript and TypeScript using finite state machines and statecharts to handle complex application logic. XState v5 supports integration with React, Vue, and Express, and provides capabilities for state persistence and hydration with MongoDB.

Tokens
60.7K
Snippets
242
Records
287
Agent score
98%

What's inside XState

  1. XState SemVer and Breaking Changes Policy

    main

    XState follows a SemVer policy with specific considerations for its runtime and TypeScript definitions:

    Runtime API

    XState aims to avoid breaking changes to the runtime API in minor or patch releases. However, because XState executes user-defined logic, changes in behavior (even if the API signature remains the same) might affect how existing machines behave. Users should always review release notes before upgrading.

    TypeScript Definitions

    The team reserves the right to adjust TypeScript definitions or drop support for older TypeScript versions in minor releases to leverage newer language features and improve type safety.

    Package Compatibility

    Most XState packages (like @xstate/react) declare xstate as a peer dependency. While updates to xstate are designed to be compatible with existing packages, it is highly recommended to update xstate whenever you update a package that depends on it (e.g., updating @xstate/react should trigger an update to xstate).

  2. Understand Machine Context in XState

    main

    In XState, Machine Context represents the extended state of a machine. It contains all the variables that the machine needs to access and modify during its lifecycle. In the Trivia Game example, the context manages game-specific data such as:

    • Current points
    • Remaining lives
    • Clues available
    • Question data

    While the machine's states track where the user is in the game flow, the context tracks the data that changes as a result of user actions or transitions.

  3. How history states work

    main

    History states allow a machine to transition back to the last active child state of a parent state. This is useful for 'back' or 'undo' functionality where you want to return to a specific sub-state within a complex workflow.

    To implement a history state, add a state with type: 'history'. When transitioning to this state, the machine will automatically determine which sub-state was active before the transition occurred and enter it.

    import { createMachine, createActor } from 'xstate';
    
    const paymentMachine = createMachine({
      id: 'payment',
      initial: 'method',
      states: {
        method: {
          initial: 'cash',
          states: {
            cash: { on: { SWITCH_CHECK: 'check' } },
            check: { on: { SWITCH_CASH: 'cash' } },
            hist: { type: 'history' } // The history state
          },
          on: { NEXT: 'review' }
        },
        review: {
          on: { PREVIOUS: 'method.hist' } // Transitioning to the history state
        }
      }
    });
    
    const actor = createActor(paymentMachine);
    actor.start();
    actor.send({ type: 'SWITCH_CHECK' }); // Now in 'method.check'
    actor.send({ type: 'NEXT' });       // Now in 'review'
    actor.send({ type: 'PREVIOUS' });   // Returns to 'method.check' via the history state
  4. How hierarchical (nested) state machines work

    main

    Hierarchical state machines allow you to define states within other states. This enables modeling complex logic by nesting child states inside a parent state. When a machine enters a parent state, it also enters its own initial child state. Transitions within the child state can be scoped to that hierarchy, or the parent can define transitions that apply to its entire state tree.

    In XState, you can achieve this by defining a states object within a state definition, effectively nesting the machine's structure.

    import { createMachine, createActor } from 'xstate';
    
    const pedestrianStates = {
      initial: 'walk',
      states: {
        walk: { on: { PED_TIMER: 'wait' } },
        wait: { on: { PED_TIMER: 'stop' } },
        stop: {}
      }
    };
    
    const lightMachine = createMachine({
      id: 'light',
      initial: 'green',
      states: {
        green: { on: { TIMER: 'yellow' } },
        yellow: { on: { TIMER: 'red' } },
        red: {
          on: { TIMER: 'green' },
          ...pedestrianStates // Nesting pedestrian states inside the 'red' state
        }
      }
    });
    
    const actor = createActor(lightMachine);
    actor.start();
    actor.send({ type: 'TIMER' });
    // The state value becomes { red: 'walk' } because it entered the 'red' state and its initial child 'walk'
  5. Understand States and Transitions in XState

    main

    An XState machine is composed of States and Transitions:

    • States: Define the current condition or location of the application (e.g., questionStart, correctAnswer, incorrectAnswer). Each state can contain logic specific to that moment in the lifecycle.
    • Transitions: Define the movement from one state to another. A transition is triggered by an event (e.g., user.selectAnswer).

    In the Trivia Game, when the machine is in the questionStart state and receives the user.selectAnswer event, the transition logic determines whether the machine moves to the correctAnswer state or the incorrectAnswer state based on the correctness of the user's choice.

  6. Persist and hydrate state machine state with MongoDB

    main

    You can achieve state persistence by hooking a MongoDB client into a running XState Actor. This pattern allows you to save a snapshot of the Actor's state to a database and retrieve it (hydrate) whenever the Actor receives an event.

    Key behaviors in this pattern:

    • The Actor is bound to a single record in the database.
    • The Actor only updates that specific record.
    • If no record exists (e.g., on the first run), a new state record is created in the database.

    This is useful for long-running stateful flows or environments where compute consistency is not guaranteed.

    IMPORTANT

    This pattern is a starting point and is not production-ready. For production use, ensure all connection strings are URI-encoded when authenticating to MongoDB.

  7. Define schemas for type safety and runtime validation

    main

    XState Store uses the Standard Schema interface for type inference. You can define context, events, and emitted schemas. By default, schemas are used for type inference but not runtime validation. To enable runtime validation, use the .with(validateSchemas()) extension.

    import { createStore } from '@xstate/store';
    import { validateSchemas } from '@xstate/store/validate';
    import { z } from 'zod';
    
    const store = createStore({
      schemas: {
        context: z.object({ count: z.number() }),
        events: { increment: z.object({ by: z.number() }) },
        emitted: { increased: z.object({ by: z.number() }) }
      },
      context: { count: 0 },
      on: {
        increment: (context, event, enqueue) => {
          enqueue.emit.increased({ by: event.by });
          return { count: context.count + event.by };
        }
      }
    }).with(validateSchemas());
  8. How parallel state machines work

    main

    Parallel state machines allow multiple statecharts to run simultaneously within a single machine. Instead of being in exactly one state at a time, the machine is in one state from each top-level region defined in the states object.

    To create a parallel machine, set the type property to 'parallel' in the machine configuration. This is useful for modeling independent orthogonal concerns, such as a text editor that tracks bold, italics, and list styles independently.

    import { createMachine, createActor } from 'xstate';
    
    const wordMachine = createMachine({
      id: 'word',
      type: 'parallel', // Enables parallel execution of all top-level states
      states: {
        bold: {
          initial: 'off',
          states: {
            off: { on: { TOGGLE_BOLD: 'on' } },
            on: { on: { TOGGLE_BOLD: 'off' } }
          }
        },
        underline: {
          initial: 'off',
          states: {
            off: { on: { TOGGLE_UNDERLINE: 'on' } },
            on: { on: { TOGGLE_UNDERLINE: 'off' } }
          }
        }
        // ... other orthogonal states
      }
    });
    
    const actor = createActor(wordMachine);
    actor.start();
    // state.value will be an object representing the current state of every region:
    // { bold: 'off', underline: 'off', ... }
  9. Match hierarchical and parallel states

    main

    When working with hierarchical or parallel machines, state values are objects rather than strings. To correctly check for specific states in your UI, use the state.matches(...) method. This is particularly effective when combined with SolidJS <Switch> and <Match> components.

    const Loader = () => {
      const [snapshot, send] = useActor(/* ... */);
    
      return (
        <div>
          <Switch fallback={null}>
            <Match when={snapshot.matches('idle')}>
              <Loader.Idle />
            </Match>
            <Match when={snapshot.matches({ loading: 'user' })}>
              <Loader.LoadingUser />
            </Match>
            <Match when={snapshot.matches({ loading: 'friends' })}>
              <Loader.LoadingFriends />
            </Match>
          </Switch>
        </div>
      );
    };
  10. Quickstart with @xstate/store-svelte

    main

    You can manage state in Svelte by creating a store with createStore and subscribing to specific parts of that state using useSelector. The useSelector hook returns a Svelte readable store, which you can access using the $ prefix in your Svelte template.

    <script>
    import { createStore, useSelector } from '@xstate/store-svelte';
    // ...
    
    const store = createStore({
      context: { count: 0 },
      on: {
        inc: (ctx) => ({ ...ctx, count: ctx.count + 1 })
      }
    });
    
    const count = useSelector(store, (s) => s.context.count);
    </script>
    
    <button on:click={() => store.send({ type: 'inc' })}>
      Count: {$count}
    </button>
  11. Quickstart with @xstate/store-angular

    main

    To use XState Store in an Angular component, create a store using createStore and then use injectStore to expose specific parts of the state as Angular signals. This allows you to react to state changes directly in your component templates.

    import { Component } from '@angular/core';
    import { createStore, injectStore } from '@xstate/store-angular';
    
    const store = createStore({
      context: { count: 0 },
      on: {
        inc: (ctx) => ({ ...ctx, count: ctx.count + 1 })
      }
    });
    
    @Component({
      selector: 'app-counter',
      template: `
        <button (click)="store.send({ type: 'inc' })">Count: {{ count() }}</button>
      `
    })
    export class CounterComponent {
      store = store;
      count = injectStore(store, (s) => s.context.count);
    }
  12. Quickstart with @xstate/store-vue

    main

    To use @xstate/store-vue in a Vue component, create a store using createStore and subscribe to specific parts of the state using the useSelector composable. This allows your component to reactively update when the selected state changes.

    <script setup>
    import { createStore, useSelector } from '@xstate/store-vue';
    // ...
    
    const store = createStore({
      context: { count: 0 },
      on: {
        inc: (ctx) => ({ ...ctx, count: ctx.count + 1 })
      }
    });
    
    const count = useSelector(store, (s) => s.context.count);
    </script>
    
    <template>
      <button @click="store.send({ type: 'inc' })">Count: {{ count }}</button>
    </template>