@xstate/fsm vs @xstate/react vs @xstate/test
Architecting State-Driven Applications with XState Utilities
@xstate/fsm@xstate/react@xstate/testSimilar Packages:

Architecting State-Driven Applications with XState Utilities

These packages represent distinct layers of the XState ecosystem: @xstate/fsm handles lightweight logic definition, @xstate/react provides bindings for React UIs, and @xstate/test enables model-based testing. Together, they allow developers to define, connect, and verify state machines across the application stack. While complementary, understanding their specific roles is critical for avoiding unnecessary dependencies, especially with the release of XState v5 where core functionality has shifted.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@xstate/fsm030,15557.1 kB1243 years agoMIT
@xstate/react030,15538 kB1247 months agoMIT
@xstate/test030,15572.7 kB124-MIT

Architecting State-Driven Applications: Logic, UI, and Testing

Building robust applications with XState involves three distinct phases: defining the logic, connecting it to the user interface, and verifying its behavior. The packages @xstate/fsm, @xstate/react, and @xstate/test each specialize in one of these areas. While they often work together, understanding their specific APIs and constraints helps you avoid over-engineering.

🧠 Defining the Logic: Standalone vs. Integrated

The foundation of any state-driven app is the machine definition. How you define and interpret this logic depends on your environment.

@xstate/fsm is designed for lightweight, framework-agnostic logic.

  • It exports createMachine and interpret.
  • Ideal for Node.js scripts, Web Workers, or non-React frontend logic.
// @xstate/fsm: Standalone definition
import { createMachine, interpret } from '@xstate/fsm';

const machine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});

const service = interpret(machine).start();
service.send('TOGGLE');

@xstate/react does not define logic itself but consumes it.

  • It expects a machine created by xstate or @xstate/fsm.
  • Focuses on binding that logic to component lifecycle.
// @xstate/react: Consuming logic
import { useMachine } from '@xstate/react';
import { toggleMachine } from './toggleMachine'; // Imported from fsm or core

function ToggleButton() {
  const [state, send] = useMachine(toggleMachine);
  return <button onClick={() => send('TOGGLE')}>{state.value}</button>;
}

@xstate/test wraps the logic for verification.

  • It takes an existing machine and creates a test model.
  • Does not run the app, but simulates paths through the logic.
// @xstate/test: Modeling for tests
import { createTestModel } from '@xstate/test';
import { toggleMachine } from './toggleMachine';

const testModel = createTestModel(toggleMachine);

// Generates test plans based on possible paths
const testPlans = testModel.getShortestPaths();

🎨 Connecting to UI: Hooks vs. Manual Subscription

When the logic needs to drive a user interface, the approach changes from manual interpretation to reactive hooks.

@xstate/fsm requires manual subscription in UI frameworks.

  • You must manually call service.subscribe and force updates.
  • Risky in React due to potential memory leaks or stale closures.
// @xstate/fsm: Manual React integration (Not Recommended)
useEffect(() => {
  const service = interpret(machine).start();
  service.subscribe(setState); // Manual subscription
  return () => service.stop(); // Manual cleanup
}, []);

@xstate/react automates subscription and cleanup.

  • useMachine handles the service lifecycle internally.
  • Ensures the component re-renders only on relevant state changes.
// @xstate/react: Automated integration
function ToggleButton() {
  // Handles start, subscribe, and stop automatically
  const [state, send] = useMachine(toggleMachine);
  return <button onClick={() => send('TOGGLE')}>{state.value}</button>;
}

@xstate/test does not connect to UI.

  • It operates in the test runner environment (Jest, Cypress).
  • Focuses on state coverage rather than DOM interaction.
// @xstate/test: Test execution
import { describe } from '@xstate/test';

describe('toggle machine', () => {
  testPlans.forEach((plan) => {
    plan.describe('should handle transitions', () => {
      plan.test('runs path', async ({ state }) => {
        // Assertions on state, not UI
        expect(state.value).toBeDefined();
      });
    });
  });
});

🛡️ Verification: Unit Tests vs. Model-Based Testing

Testing state machines can be done via traditional unit tests or model-based testing (MBT).

@xstate/fsm relies on traditional unit tests.

  • You manually send events and assert the resulting state.
  • Good for simple logic, but misses complex path coverage.
// @xstate/fsm: Manual Unit Test
it('toggles state', () => {
  const service = interpret(machine).start();
  service.send('TOGGLE');
  expect(service.getSnapshot().value).toBe('active');
});

@xstate/react is tested via React Testing Library.

  • You test the component behavior, not the machine directly.
  • Indirect verification of state logic through UI interactions.
// @xstate/react: Component Test
it('renders active state', async () => {
  render(<ToggleButton />);
  fireEvent.click(screen.getByRole('button'));
  expect(screen.getByText('active')).toBeInTheDocument();
});

@xstate/test automates path coverage.

  • Generates tests for every valid transition path.
  • Catches unreachable states or invalid transitions automatically.
// @xstate/test: Model-Based Test
const testPlans = testModel.getShortestPaths();

testPlans.forEach(plan => {
  plan.test('covers all paths', async ({ state }) => {
    // Automatically asserts validity of each step
    expect(state.matches('active') || state.matches('inactive')).toBe(true);
  });
});

🔄 The XState v5 Shift: Core vs. FSM

A critical architectural decision today is choosing between @xstate/fsm and the core xstate package.

@xstate/fsm is the v4 lightweight solution.

  • Still maintained for legacy v4 projects.
  • Lacks some advanced v5 features like actors and persistence.
// @xstate/fsm: v4 Style
import { createMachine } from '@xstate/fsm';
// Limited to FSM features only

@xstate/react supports both v4 and v5.

  • In v5, it leans heavily on useActor for concurrent logic.
  • Remains the standard for React integration regardless of core version.
// @xstate/react: v5 Style
import { useActor } from '@xstate/react';
// Works with xstate v5 actors

@xstate/test is compatible with v5 machines.

  • Works with the new xstate core package definitions.
  • Recommended to pair with v5 for better type inference.
// @xstate/test: v5 Compatibility
import { createTestModel } from '@xstate/test';
import { createMachine } from 'xstate'; // v5 core

const model = createTestModel(createMachine({ /*...*/ }));

📊 Summary: Package Roles

PackagePrimary RoleBest ForV5 Status
@xstate/fsmLogic DefinitionNon-React, v4 LegacySuperseded by xstate
@xstate/reactUI BindingReact AppsActive / Standard
@xstate/testVerificationComplex FlowsActive / Compatible

💡 The Big Picture

@xstate/fsm is a specialized tool for logic isolation. Use it when you need state logic outside the UI layer or are stuck on v4. However, for most new projects, the core xstate package is the better choice as it offers the same lightweight benefits with modern features.

@xstate/react is the bridge between your logic and your view. It is essential for React developers using XState, as it removes the boilerplate of managing subscriptions and ensures your UI stays in sync with your state.

@xstate/test is the safety net. It transforms testing from a manual chore into an automated guarantee. If your state machine controls critical business logic (like payments or onboarding), this package pays for itself by catching edge cases you wouldn't think to test manually.

Final Thought: These packages are not competitors; they are layers. A robust architecture often uses xstate (core) for logic, @xstate/react for the view, and @xstate/test for quality assurance. Choose based on the layer you are building.

How to Choose: @xstate/fsm vs @xstate/react vs @xstate/test

  • @xstate/fsm:

    Choose @xstate/fsm if you are maintaining an XState v4 project or need a standalone, lightweight finite state machine without React dependencies. For new greenfield projects, prefer the core xstate package (v5) which now includes these capabilities natively with better tree-shaking.

  • @xstate/react:

    Choose @xstate/react when building React applications that need to subscribe to state machine changes. It provides optimized hooks like useMachine and useActor that handle subscription cleanup and render cycles automatically, preventing common memory leaks.

  • @xstate/test:

    Choose @xstate/test when you require rigorous, model-based testing for complex flows. It is ideal for generating test plans automatically from your machine definition, ensuring edge cases and invalid transitions are caught before deployment.

README for @xstate/fsm

@xstate/fsm


XState FSM
XState for Finite State Machines

This package contains a minimal, 1kb implementation of XState for finite state machines.

Features

@xstate/fsmXState
Finite states✅✅
Initial state✅✅
Transitions (object)✅✅
Transitions (string target)✅✅
Delayed transitions❌✅
Eventless transitions❌✅
Wildcard transitions✅✅
Nested states❌✅
Parallel states❌✅
History states❌✅
Final states❌✅
Context✅✅
Entry actions✅✅
Exit actions✅✅
Transition actions✅✅
Parameterized actions❌✅
Transition guards✅✅
Parameterized guards❌✅
Spawned actors❌✅
Invoked actors❌✅
  • Finite states (non-nested)
  • Initial state
  • Transitions (object or strings)
  • Context
  • Entry actions
  • Exit actions
  • Transition actions
  • state.changed

If you want to use statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, transient transitions, etc. please use XState.

Quick start

Installation

npm i @xstate/fsm

Usage (machine)

import { createMachine } from '@xstate/fsm';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});

const { initialState } = toggleMachine;

const toggledState = toggleMachine.transition(initialState, 'TOGGLE');
toggledState.value;
const untoggledState = toggleMachine.transition(toggledState, 'TOGGLE');
untoggledState.value;
// => 'inactive'

Usage (service)

import { createMachine, interpret } from '@xstate/fsm';

const toggleMachine = createMachine({});

const toggleService = interpret(toggleMachine).start();

toggleService.subscribe((state) => {
  console.log(state.value);
});

toggleService.send('TOGGLE');
toggleService.send('TOGGLE');
toggleService.stop();