XState logo

XState

Deterministic state machines and statecharts for robust application logic.

npm install xstate
29.0K2.5M/weekv5.24.02.09 MBMIT169 issues
Last updated: 2025-11-04
Star history chart for statelyai/xstate

TL;DR

The industry-standard library for creating, interpreting, and executing finite state machines and statecharts in JavaScript.

It allows you to model your application logic as a graph of states and transitions, making complex behaviors deterministic, visualizable, and testable.

Why XState?

XState solves a different problem than most state management libraries. While Redux or Zustand focus on storing data, XState focuses on orchestrating behavior. It prevents bugs by mathematically proving which transitions are allowed at any given moment.

  • Eliminate Impossible States: By defining finite states (e.g., idle, loading, success), you enforce that your app can never be in two conflicting states at once (like showing a spinner and an error message).
  • Visual Documentation: XState machines can be copy-pasted into the XState Visualizer to generate instant, interactive diagrams of your logic.
  • Deterministic Logic: State transitions are explicit. An event CLICK while in loading state can be explicitly ignored, preventing race conditions or double-submissions.
  • Framework Agnostic: Your business logic is just a plain object/function, meaning it can be shared between React, Vue, Node.js, or even different projects.
  • Actor Model: XState v5 treats logic as "actors" that can communicate, making it powerful for managing complex, asynchronous workflows involving multiple processes.

Code Snippet

Here is a classic "Fetch Machine" that handles the loading states, errors, and retry logic explicitly.

import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';

// 1. Define the Machine
const fetchMachine = createMachine({
  id: 'fetch',
  initial: 'idle',
  context: {
    data: null,
    error: null
  },
  states: {
    idle: {
      on: { FETCH: 'loading' }
    },
    loading: {
      // Invoke a Promise (an Actor)
      invoke: {
        src: 'fetchData',
        onDone: {
          target: 'success',
          actions: assign({ data: ({ event }) => event.output })
        },
        onError: {
          target: 'failure',
          actions: assign({ error: ({ event }) => event.error })
        }
      }
    },
    success: {
      on: { RETRY: 'loading' }
    },
    failure: {
      on: { RETRY: 'loading' }
    }
  }
});

// 2. Use in Component
export const DataFetcher = () => {
  const [state, send] = useMachine(fetchMachine, {
    actors: {
      fetchData: async () => {
        const res = await fetch('/api/data');
        return res.json();
      }
    }
  });

  if (state.matches('loading')) return <div>Loading...</div>;
  if (state.matches('failure')) return (
    <div>
      Error: {state.context.error.message}
      <button onClick={() => send({ type: 'RETRY' })}>Retry</button>
    </div>
  );
  if (state.matches('success')) return <div>Data: {state.context.data.title}</div>;

  return <button onClick={() => send({ type: 'FETCH' })}>Load Data</button>;
};

The power here is that send({ type: 'FETCH' }) will do absolutely nothing if the machine is already in the loading state, automatically preventing double-fetch bugs.

Pros and Cons

No library is perfect; understanding the trade-offs is key to selecting the right tool.

Pros

  • Bulletproof Logic: Forces you to handle edge cases (like what happens if the user clicks "Submit" while already submitting).
  • Visualizer & Tooling: The Stately visual editor allows designers and developers to collaborate on logic flows without writing code.
  • Testability: You can generate integration tests automatically by traversing the paths of your state machine.

Cons

  • Steep Learning Curve: Concepts like guards, actions, context, and actors require a significant shift in mental model compared to simple useState.
  • Verbosity: Defining a machine requires much more boilerplate code than a simple function or hook, which can feel like overkill for simple toggles.
  • Complexity Overhead: For simple CRUD apps, XState adds a layer of abstraction that might slow down initial development velocity.

Comparison with Other State Management Libraries

The table below outlines the positioning differences between XState and other popular State Management libraries to help you make an informed decision:

LibraryDesign PhilosophyBest ForPain Points
XStateState Machines
Model logic as a directed graph of states and transitions.
Complex Flows
Multi-step forms, checkout processes, media players, and game logic.
Learning Curve
Requires learning a specific vocabulary and mathematical concept (FSM).
ReduxFlux Architecture
Centralized global store with explicit actions and reducers.
Global Data
Sharing data across the entire application with strict predictability.
Boilerplate
Can require significant setup code for relatively simple logic.
ZustandMinimalist
Unopinionated, hook-based global state.
Simple State
Quickly spinning up global state for UI settings or simple data.
Structure
Logic is often mixed directly inside components or hooks, harder to visualize.

Verdict: When to Adopt

Adopt XState when your application logic is complex enough that you are struggling to keep track of isLoading, isError, and isOpen boolean flags. It is the premier choice for wizards, multi-step authentication flows, and critical business logic where bugs are unacceptable. For simple global data storage (like a theme toggle), it is likely overkill.