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.
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.
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.
createMachine and interpret.// @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.
xstate or @xstate/fsm.// @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.
// @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();
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.
service.subscribe and force updates.// @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.// @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.
// @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();
});
});
});
});
Testing state machines can be done via traditional unit tests or model-based testing (MBT).
@xstate/fsm relies on traditional unit tests.
// @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.
// @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.
// @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);
});
});
A critical architectural decision today is choosing between @xstate/fsm and the core xstate package.
@xstate/fsm is the v4 lightweight solution.
// @xstate/fsm: v4 Style
import { createMachine } from '@xstate/fsm';
// Limited to FSM features only
@xstate/react supports both v4 and v5.
useActor for concurrent logic.// @xstate/react: v5 Style
import { useActor } from '@xstate/react';
// Works with xstate v5 actors
@xstate/test is compatible with v5 machines.
xstate core package definitions.// @xstate/test: v5 Compatibility
import { createTestModel } from '@xstate/test';
import { createMachine } from 'xstate'; // v5 core
const model = createTestModel(createMachine({ /*...*/ }));
| Package | Primary Role | Best For | V5 Status |
|---|---|---|---|
@xstate/fsm | Logic Definition | Non-React, v4 Legacy | Superseded by xstate |
@xstate/react | UI Binding | React Apps | Active / Standard |
@xstate/test | Verification | Complex Flows | Active / Compatible |
@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.
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.
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.
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.
XState for Finite State Machines
This package contains a minimal, 1kb implementation of XState for finite state machines.
| @xstate/fsm | XState | |
|---|---|---|
| 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 | ❌ | ✅ |
state.changedIf 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.
npm i @xstate/fsm
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'
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();