No history yet

Finite State Machines

Beyond Boolean Logic

In complex components, state management often degrades into a web of isLoading, isError, isSuccess, and other boolean flags. This implicit state management creates impossible states, where, for instance, isLoading and isError might both be true. The logic becomes brittle and difficult to reason about. Explicit state machines solve this by defining a finite set of states and the deterministic transitions between them.

A finite state machine can only be in one state at a time, eliminating the possibility of contradictory or impossible states.

While a basic FSM enforces mutually exclusive states, it lacks the expressiveness for complex UI. This is where statecharts, a formalism introduced by David Harel, extend FSMs. Statecharts add hierarchy, parallelism (orthogonal regions), and a history mechanism, making them far more scalable for modeling application logic. XState is a JavaScript library that implements the statechart formalism.

Hierarchical and Parallel States

Hierarchical, or nested, states allow a parent state to contain its own sub-machine. This is crucial for refining behavior. For example, a fetching state might have its own internal states like validatingRequest and awaitingResponse. An event handled by the parent state can be handled without the child states needing to be aware of it, simplifying the overall logic.

Parallel states, or orthogonal regions, allow a machine to be in multiple states simultaneously, as long as those states belong to different regions. Consider a text editor component. It might have a mode region with states like editing and viewing, and a style region with states like bold and italic. A user can be in the editing and bold states at the same time. This prevents a combinatorial explosion of states (editing_bold, editing_italic, etc.).

Typed Machines and the Actor Model

Manually maintaining type safety in a state machine is error-prone. XState's Typegen solves this by automatically generating TypeScript types from the machine definition itself. This provides autocomplete for states, events, and context, ensuring that you cannot send an invalid event or access a context property that doesn't exist in the current state.

import { createMachine } from 'xstate';

// The machine definition provides the source of truth for types.
const promiseMachine = createMachine({
  id: 'promise',
  initial: 'pending',
  // With Typegen, context, events, etc. will be strictly typed.
  context: {
    data: undefined as string | undefined,
    error: undefined as string | undefined
  },
  // This comment is read by the XState VSCode extension to enable Typegen
  tsTypes: {} as import('./machine.typegen').Typegen0,
  states: {
    pending: {
      on: {
        RESOLVE: 'fulfilled',
        REJECT: 'rejected'
      }
    },
    fulfilled: {
      type: 'final',
      entry: 'setData'
    },
    rejected: {
      type: 'final',
      entry: 'setError'
    }
  }
});

For communication between machines, XState implements the Actor model. Instead of directly manipulating the state of another component, you spawn actors (running instances of machines) and send them events. The parent machine can invoke or spawn child machines, subscribe to their state changes, and send them messages. This creates a clear, hierarchical system of communication that is more predictable and debuggable than direct method calls or a global event bus.

In the Actor model, actors are completely isolated; they do not share state. They communicate only through asynchronous messages (events).

Guards, Actions, and Services

State transitions are not always unconditional. Guards are functions that determine whether a transition should occur based on the current context or event payload. A guard must be a pure function that returns a boolean, enabling or disabling a path without causing side effects.

'FETCH': { target: 'loading', cond: 'isValidRequest' }

When a transition occurs, or upon entering or exiting a state, you often need to execute side effects. This is the role of actions. Actions are fire-and-forget functions for synchronous side effects, like updating the machine's context.

entry: assign({ user: (_, event) => event.user })

For asynchronous side effects like API calls or timers, XState uses services. A service is an invoked process that can send events back to the parent machine. XState has built-in support for invoking Promises, Callbacks, Observables, and other machines.

This separation is critical: states and transitions define the when, guards determine the if, and actions and services define the what.

{
  id: 'user',
  initial: 'idle',
  context: { userId: null, userData: null, error: null },
  states: {
    idle: {
      on: {
        FETCH: {
          target: 'loading',
          // Guard: only fetch if a userId is present
          cond: (context) => context.userId !== null
        }
      }
    },
    loading: {
      // Service: invoke an async function that returns a promise
      invoke: {
        id: 'getUser',
        src: 'fetchUserData',
        onDone: {
          target: 'success',
          // Action: assign the result to context
          actions: assign({ userData: (_, event) => event.data })
        },
        onError: {
          target: 'failure',
          actions: assign({ error: (_, event) => event.data })
        }
      }
    },
    success: { type: 'final' },
    failure: { on: { RETRY: 'loading' } }
  }
}

By embracing these patterns, you move from managing state with scattered booleans and useEffect hooks to a centralized, visual, and type-safe orchestration of your component's logic.

Quiz Questions 1/6

What is the primary problem that explicit state machines solve compared to managing state with multiple boolean flags like isLoading, isError, and isSuccess?

Quiz Questions 2/6

In a UI component for a rich text editor, you need to manage the document's mode (editing or viewing) and its current text style (bold or italic) independently. A user can be editing while the text is bold. Which statechart feature is best suited for this scenario?