Robot Finite State Machine

repository·main·Indexed 24 days ago

https://github.com/matthewp/robot

A small, functional, and immutable Finite State Machine (FSM) library for declarative application state. Robot provides a composition-based approach to building state machines via robot3 and includes official integrations for React (react-robot), Preact (preact-robot), Haunted (haunted-robot), and Lit (lit-robot), as well as general hook utilities in robot-hooks.

Tokens
24.2K
Snippets
77
Records
114
Agent score
77%

What's inside Robot

  1. What is Robot?

    main
    Robot is a small, functional, and immutable Finite State Machine (FSM) library. It allows you to use state machines to bring a declarative programming approach to your application state, making component logic more predictable and structured.
  2. What is an `invoke` state and how to use it

    main

    An invoke state is a special type of state that immediately executes a Promise-returning function or another machine. It is used to handle asynchronous tasks or delegate logic to child machines.

    When the invoked task completes, the service sends specific events (done or error) which you can capture using transition to move to new states and reduce to update the machine context.

    Key characteristics:

    • The argument passed to invoke must be a function that returns a Promise, not the Promise itself.
    • If a Promise is already created, wrap it in a function: () => promise.
    • Successful resolution triggers a done event with a data property.
    • Rejection triggers an error event with an error property.
    import { createMachine, immediate, invoke, reduce, state, transition } from 'robot3';
    
    async function loadUsers() {
      return [ { id: 1, name: 'Wilbur' } ];
    }
    
    const machine = createMachine({
      idle: state(
        transition('load', 'loading')
      ),
      loading: invoke(loadUsers,
        transition('done', 'idle',
          reduce((ctx, ev) => ({ ...ctx, user: ev.data }))
        ),
        transition('error', 'error',
          reduce((ctx, ev) => ({ ...ctx, error: ev.error }))
        )
      ),
      error: state()
    })
  3. How transitions work in Robot

    main

    Transitions define the pathways between states in a finite state machine. Instead of allowing state changes to happen anywhere in your code (imperative programming), transitions explicitly declare how a machine moves from one state to another in response to specific events. This makes application behavior predictable and prevents invalid state changes.

    Transitions are defined within a state using the transition function. A transition consists of:

    1. Event name: A string identifying the trigger.
    2. Target state: The name of the state to move to (must be a valid state defined in the machine).
    3. Optional modifiers: Guards, actions, or reducers.

    Self-Transitions

    A transition can target its own state. This is a common pattern used to update context (data) without changing the current state of the machine.

    import { createMachine, state, transition } from 'robot3';
    
    const machine = createMachine({
      idle: state(
        transition('fetch', 'loading')
      ),
      loading: state(
        transition('success', 'loaded'),
        transition('error', 'error')
      ),
      loaded: state(),
      error: state(
        transition('retry', 'loading')
      )
    });
  4. Use guards to control state transitions

    main

    A guard is a method used within a transition to determine if the transition is allowed to proceed.

    • If the guard function returns true, the transition occurs and the machine moves to the target state.
    • If the guard function returns false, the transition is prevented, and the machine remains in its current state.

    Guards typically receive the current context (ctx) as an argument to evaluate conditions based on the machine's state.

    import { createMachine, guard, state, transition } from 'robot3';
    
    // Only allow submission if a login and password is entered.
    function canSubmit(ctx) {
      return ctx.login && ctx.password;
    }
    
    const machine = createMachine({
      idle: state(
        transition('submit', 'complete',
          guard(canSubmit)
        )
      ),
      complete: state()
    });
  5. Guard execution timing and lifecycle

    main

    Guards execute before any actions or reducers in a transition. This provides several guarantees:

    • Guards see the old context (before any actions modify it).
    • Actions and reducers only run if the guards pass.
    • State changes only occur if the guards pass.
    const machine = createMachine({
      idle: state(
        transition('submit', 'processing',
          guard((ctx) => {
            console.log('Guard checking:', ctx.value);  // Sees old value
            return ctx.value > 0;
          }),
          reduce((ctx) => {
            console.log('Reducer running:', ctx.value);  // Only runs if guard passes
            return { ...ctx, value: ctx.value + 1 };
          })
        )
      ),
      processing: state()
    }, () => ({ value: 5 }));
  6. What are Actions in Robot

    main
    Actions are side effects that occur during state transitions. They allow you to update the machine's context, make API calls, manipulate the DOM, log events, or perform any other effect when moving between states. In Robot, state transitions should remain pure and declarative; actions provide a controlled way to handle the side effects required by real-world applications without cluttering the core state machine logic.
  7. Robot integrations and ecosystem

    main

    Robot is designed to work with various UI libraries. It provides official integrations for:

    • React (react-robot)
    • Preact (preact-robot)
    • Haunted (haunted-robot)
    • Lit (lit-robot)

    Additionally, you can use robot3-viz to visualize your robot state machines.

  8. Compose reusable machine parts using Robot

    main

    Robot follows a 'composition over configuration' philosophy, allowing you to break down Finite State Machines into small, reusable functions rather than constructing a single large configuration object.

    Instead of defining every state and transition inside a single createMachine call, you can create helper functions that return transition or state definitions. These helpers can be exported from separate modules and imported into different machines to maintain succinct and DRY (Don't Repeat Yourself) code.

    Common patterns include creating generic field handlers that listen for specific events, run a reduce function to update the machine context, and return to a specific state.

    import { createMachine, state, reduce, transition } from 'robot3';
    
    // A reusable helper function that returns a transition
    const field = (prop, state) => (
      transition(prop, state,
        reduce((ctx, ev) => ({ ...ctx, [prop]: ev.event.target.value }))
      )
    );
    
    // A specialized helper for a specific state
    const formField = (prop) => field(prop, 'form');
    
    const machine = createMachine({
      form: state(
        formField('first'),
        formField('last')
      )
    });
  9. How to model parallel states in Robot

    main

    Robot does not support a native parallel state type. Instead of one machine with multiple independent states, the recommended pattern is to model these as multiple separate machines. This maintains the principle of composition and keeps machines simple.

    const toggleMachine = () => createMachine({
      inactive: state(
        transition('toggle', 'active')
      ),
      active: state(
        transition('toggle', 'inactive')
      )
    });
    
    const bold = toggleMachine();
    const italic = toggleMachine();
    const underline = toggleMachine();
  10. Understand the transition execution order

    main

    When an event is sent to a machine, the transition process follows a strict, predictable order:

    1. Event received: The machine receives the event.
    2. Transition matched: The machine finds a transition with the matching event name.
    3. Guards checked: If guards are defined, they are evaluated. The transition only proceeds if the guard passes.
    4. Actions executed: Any actions or reducers associated with the transition are run.
    5. State changed: The machine moves to the target state.
    6. Context updated: Any changes made to the context via actions or reducers take effect.
  11. How different event types work in Robot

    main

    Robot supports several ways for events to trigger transitions:

    1. User Events: Explicitly sent via service.send(). These represent manual triggers like button clicks.
    2. Immediate Transitions: Transitions that trigger automatically upon entering a state using the immediate() function, without requiring an external event.
    3. Invoked Events: When using invoke for asynchronous operations, Robot automatically generates done and error events based on the outcome of the promise.
    import { createMachine, state, transition, immediate, invoke } from 'robot3';
    
    const machine = createMachine({
      // Immediate Transition
      validate: state(
        immediate('loaded', guard(isValid)),
        immediate('error', guard(isInvalid))
      ),
      
      // Invoked Events (automatic 'done' and 'error')
      loading: invoke(fetchUsers,
        transition('done', 'loaded'),
        transition('error', 'error')
      )
    });
  12. Distinguish between State and Context

    main

    When building machines, distinguish between the mode of the application and the data it holds:

    • State: Represents the current mode or phase (e.g., loading, idle). It determines where you are in the flow.
    • Context: Represents data that persists across states (e.g., users, errorMessage). It determines what data you have available.

    Context is provided as a function that returns an object passed as the second argument to createMachine.

    const machine = createMachine({
      idle: state(
        transition('fetch', 'loading')
      ),
      loading: state(
        transition('success', 'loaded')
      ),
      loaded: state()
    }, () => ({
      // This is context
      users: [],
      errorMessage: null,
      retryCount: 0
    }));