Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:01:45 +08:00
commit 23371433fd
17 changed files with 6735 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# XState v5 Reference Documentation
Comprehensive reference materials for XState v5 implementation and analysis in **backend applications**.
## Available References
### [Core API](./core-api.md)
Complete API reference for XState v5 including:
- Machine creation (`createMachine`, `setup`)
- Actor management (`createActor`, lifecycle methods)
- State configuration and transitions
- Actions, guards, and services
- Utility functions and type helpers
### [Actors](./actors.md)
Deep dive into the actor model:
- Actor types (state machine, promise, callback, transition, observable)
- Actor lifecycle and communication
- Spawning vs invoking actors
- Actor systems and orchestration
- Error handling and persistence
### [Common Patterns](./patterns.md)
Production-ready patterns and solutions:
- Loading states with retry logic
- Form validation and submission
- Authentication flows
- Pagination and data fetching
- Wizard/stepper interfaces
- Parallel states and history
- Debouncing and queue processing
- Modal/dialog management
- Connection handling with reconnection
### [TypeScript Integration](./typescript.md)
Complete TypeScript usage guide:
- Setup pattern with strong typing
- Type inference and helpers
- Generic machine factories
- Event assertions and discriminated unions
- Backend service types
- Migration from v4 typegen
### [Testing Strategies](./testing.md)
Backend testing approaches:
- Unit testing state machines
- Testing with xstate-audition
- Mocking external services and databases
- Testing async backend operations
- Performance testing for server environments
- Best practices for backend testing
## Quick Start
For new XState v5 implementations, start with:
1. **[TypeScript Integration](./typescript.md)** - Set up proper typing
2. **[Core API](./core-api.md)** - Learn the fundamental APIs
3. **[Common Patterns](./patterns.md)** - Apply proven solutions
4. **[Testing Strategies](./testing.md)** - Ensure reliability
## External Resources
- [Official XState Documentation](https://stately.ai/docs/xstate)
- [XState Visualizer](https://stately.ai/viz)
- [XState Catalog](https://xstate-catalogue.com/)
- [xstate-audition](https://boneskull.github.io/xstate-audition/) - Model-based testing
## Version Requirements
- **XState**: v5.x
- **TypeScript**: 5.0 or greater
- **Node.js**: 18+ recommended
## Contributing
To add or update reference documentation:
1. Follow the existing markdown structure
2. Include practical code examples
3. Document both best practices and anti-patterns
4. Keep examples TypeScript-first
5. Test all code examples for accuracy

View File

@@ -0,0 +1,548 @@
# XState v5 Actors Reference
## Actor Model Fundamentals
Actors are independent entities that:
- Maintain private, encapsulated state
- Communicate via asynchronous message passing (events)
- Process messages sequentially from an internal mailbox
- Can create new child actors
- Can only expose internal state through events or snapshots
## Actor Types Overview
| Actor Type | Receives Events | Sends Events | Spawns Actors | Has Output | Use Case |
| ------------- | --------------- | ------------ | ------------- | ---------- | ------------------- |
| State Machine | Yes | Yes | Yes | Yes | Complex state logic |
| Promise | No | Yes | No | Yes | Async operations |
| Transition | Yes | Yes | No | No | Pure reducers |
| Callback | Yes | Yes | No | No | Imperative logic |
| Observable | No | Yes | No | No | Streams |
## Creating and Managing Actors
### Basic Actor Creation
```typescript
import { createActor } from 'xstate';
const actor = createActor(someLogic, {
id: 'myActor',
input: { initialData: 'value' },
snapshot: previousSnapshot, // For restoration
systemId: 'mySystem',
logger: console.log,
});
// Lifecycle
actor.start(); // Must call to begin processing
actor.stop(); // Stops actor and all children
```
### Actor Subscription
```typescript
// Simple observer function
const subscription = actor.subscribe((snapshot) => {
console.log('State:', snapshot.value);
console.log('Context:', snapshot.context);
});
// Full observer object
actor.subscribe({
next(snapshot) {
console.log('New snapshot:', snapshot);
},
error(err) {
console.error('Actor error:', err);
},
complete() {
console.log('Actor completed');
},
});
// Cleanup
subscription.unsubscribe();
```
### Snapshot Access
```typescript
// Synchronous snapshot access
const currentSnapshot = actor.getSnapshot();
// Persistable snapshot (serializable)
const persistedSnapshot = actor.getPersistedSnapshot();
localStorage.setItem('actor-state', JSON.stringify(persistedSnapshot));
```
## State Machine Actors
The most powerful actor type, implementing full statechart semantics.
```typescript
import { createMachine, createActor } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
actions: assign({ count: ({ context }) => context.count + 1 }),
},
},
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
const actor = createActor(toggleMachine);
actor.start();
actor.send({ type: 'TOGGLE' });
```
## Promise Actors
Handle single async operations that resolve or reject.
```typescript
import { fromPromise, createActor } from 'xstate';
// Basic promise actor
const fetchLogic = fromPromise(async () => {
const response = await fetch('/api/data');
return response.json();
});
// With input parameter
const fetchWithInput = fromPromise(async ({ input }) => {
const response = await fetch(`/api/data/${input.id}`);
return response.json();
});
// With abort signal
const cancellableFetch = fromPromise(async ({ input, signal }) => {
const response = await fetch('/api/data', { signal });
return response.json();
});
// Usage
const actor = createActor(fetchWithInput, {
input: { id: '123' },
});
actor.subscribe({
next: (snapshot) => {
if (snapshot.status === 'done') {
console.log('Result:', snapshot.output);
}
},
error: (err) => console.error('Failed:', err),
});
actor.start();
```
## Transition Actors
Pure reducers that transform state based on events.
```typescript
import { fromTransition, createActor } from 'xstate';
const counterLogic = fromTransition(
(state, event) => {
switch (event.type) {
case 'INC':
return { ...state, count: state.count + 1 };
case 'DEC':
return { ...state, count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
},
{ count: 0 }, // Initial state
);
const actor = createActor(counterLogic);
actor.start();
actor.send({ type: 'INC' });
console.log(actor.getSnapshot().context.count); // 1
```
## Callback Actors
Imperative actors for complex side effects and cleanup.
```typescript
import { fromCallback, createActor } from 'xstate';
const intervalLogic = fromCallback(({ sendBack, receive, input }) => {
// Setup
console.log('Starting with input:', input);
const interval = setInterval(() => {
sendBack({ type: 'TICK', timestamp: Date.now() });
}, 1000);
// Event handler
receive((event) => {
if (event.type === 'STOP_TICKING') {
clearInterval(interval);
}
});
// Cleanup function (called on stop)
return () => {
clearInterval(interval);
console.log('Cleaned up');
};
});
const actor = createActor(intervalLogic, {
input: { message: 'Timer started' },
});
actor.subscribe((snapshot) => {
console.log('Snapshot:', snapshot);
});
actor.start();
// Later: actor.send({ type: 'STOP_TICKING' });
// Or: actor.stop();
```
## Observable Actors
Integration with reactive streams (RxJS, etc.).
```typescript
import { fromObservable, createActor } from 'xstate';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
const timerLogic = fromObservable(() =>
interval(1000).pipe(map((i) => ({ type: 'TICK', count: i }))),
);
const actor = createActor(timerLogic);
actor.subscribe((snapshot) => {
console.log('Event from observable:', snapshot.context);
});
actor.start();
```
## Invoking vs Spawning Actors
### Invoked Actors (State-based)
Tied to state lifecycle - start on entry, stop on exit.
```typescript
const machine = createMachine({
states: {
loading: {
invoke: {
id: 'fetcher',
src: fromPromise(async () => {
const res = await fetch('/api');
return res.json();
}),
input: ({ context }) => ({ url: context.apiUrl }),
onDone: {
target: 'success',
actions: assign({
data: ({ event }) => event.output,
}),
},
onError: {
target: 'failure',
actions: assign({
error: ({ event }) => event.error,
}),
},
onSnapshot: {
actions: ({ event }) => {
console.log('Invoked actor snapshot:', event.snapshot);
},
},
},
},
success: {},
failure: {},
},
});
```
### Spawned Actors (Action-based)
Created dynamically, managed manually.
```typescript
import { spawn, stopChild } from 'xstate';
const parentMachine = createMachine({
context: {
todos: [],
todoActors: {},
},
on: {
ADD_TODO: {
actions: assign({
todoActors: ({ context, spawn }) => {
const id = Math.random().toString();
const todoActor = spawn(todoMachine, {
id: `todo-${id}`,
input: { text: event.text },
syncSnapshot: true, // Sync snapshots with parent
});
return {
...context.todoActors,
[id]: todoActor,
};
},
}),
},
REMOVE_TODO: {
actions: [
stopChild(({ context, event }) => `todo-${event.id}`),
assign({
todoActors: ({ context, event }) => {
const { [event.id]: removed, ...rest } = context.todoActors;
return rest;
},
}),
],
},
},
});
```
## Actor Communication
### Sending Between Actors
```typescript
import { sendTo, sendParent } from 'xstate';
const childMachine = createMachine({
entry: [
// Send to parent
sendParent({ type: 'CHILD_READY' }),
// Or use emit (preferred in v5)
emit({ type: 'CHILD_READY' }),
],
on: {
PING: {
actions: emit({ type: 'PONG' }),
},
},
});
const parentMachine = createMachine({
invoke: {
id: 'child',
src: childMachine,
},
on: {
SEND_TO_CHILD: {
actions: sendTo('child', { type: 'PING' }),
},
PONG: {
actions: log('Received pong from child'),
},
},
});
```
### Actor System
```typescript
// Access system from any actor
const actor = createActor(machine);
actor.start();
// Get actor from system
const childActor = actor.system.get('childId');
// Inspect all actors in system
actor.system._set.forEach((actor, id) => {
console.log(`Actor ${id}:`, actor.getSnapshot());
});
```
## Waiting for Actors
```typescript
import { waitFor } from 'xstate';
// Wait for condition
const doneSnapshot = await waitFor(
actor,
(snapshot) => snapshot.status === 'done',
{ timeout: 5000 }, // Optional timeout
);
// Convert to promise
import { toPromise } from 'xstate';
const result = await toPromise(actor);
// Resolves with output when done
// Rejects with error when failed
```
## Error Handling
### Promise Actor Errors
```typescript
const errorProneLogic = fromPromise(async () => {
throw new Error('Something went wrong');
});
const machine = createMachine({
invoke: {
src: errorProneLogic,
onError: {
target: 'errorState',
actions: ({ event }) => {
console.error('Error:', event.error);
},
},
},
});
```
### Callback Actor Error Reporting
```typescript
const callbackWithErrors = fromCallback(({ sendBack }) => {
try {
// risky operation
} catch (error) {
sendBack({ type: 'error.platform', error });
}
});
```
## Actor Persistence
```typescript
// Save actor state
const actor = createActor(machine);
actor.subscribe(() => {
const snapshot = actor.getPersistedSnapshot();
localStorage.setItem('actor-state', JSON.stringify(snapshot));
});
actor.start();
// Restore actor state
const savedState = localStorage.getItem('actor-state');
if (savedState) {
const restoredActor = createActor(machine, {
snapshot: JSON.parse(savedState),
});
restoredActor.start();
}
```
## Best Practices
### 1. Choose the Right Actor Type
- **State machines**: Complex logic with multiple states
- **Promises**: Single async operations
- **Transitions**: Pure state reducers
- **Callbacks**: Imperative code with cleanup
- **Observables**: Stream integrations
### 2. Actor Lifecycle Management
```typescript
// Always subscribe before starting
const subscription = actor.subscribe(observer);
actor.start();
// Clean up properly
subscription.unsubscribe();
actor.stop();
```
### 3. Error Boundaries
```typescript
// Always handle errors in parent
const parentMachine = createMachine({
invoke: {
src: childLogic,
onError: {
actions: [
log('Child failed'),
assign({ error: ({ event }) => event.error }),
],
},
},
});
```
### 4. Input Validation
```typescript
const actorWithInput = fromPromise(async ({ input }) => {
// Validate input
if (!input?.id) {
throw new Error('ID required');
}
return fetchData(input.id);
});
```
### 5. Sync vs Async Snapshots
```typescript
// Spawned actors can sync snapshots with parent
spawn(childMachine, {
id: 'child',
syncSnapshot: true, // Parent re-renders on child changes
});
```
## Testing Actors
```typescript
import { createActor } from 'xstate';
import { expect, test } from 'vitest';
test('actor processes events correctly', async () => {
const actor = createActor(machine);
const states = [];
actor.subscribe((snapshot) => {
states.push(snapshot.value);
});
actor.start();
actor.send({ type: 'EVENT1' });
actor.send({ type: 'EVENT2' });
expect(states).toEqual(['initial', 'state1', 'state2']);
});
test('promise actor resolves', async () => {
const actor = createActor(promiseLogic);
actor.start();
const result = await toPromise(actor);
expect(result).toEqual({ data: 'expected' });
});
```

View File

@@ -0,0 +1,495 @@
# XState v5 Core API Reference
## Machine Creation
### `createMachine(config)`
Creates a state machine configuration.
```typescript
import { createMachine } from 'xstate';
const machine = createMachine({
id: 'machineId', // Unique identifier
initial: 'stateName', // Initial state
context: {}, // Initial context data
types: {}, // TypeScript type definitions
states: {}, // State definitions
on: {}, // Global transitions
entry: [], // Entry actions
exit: [], // Exit actions
after: {}, // Delayed transitions
always: [], // Eventless transitions
invoke: {}, // Invoked services
tags: [], // State tags
description: '', // Machine description
});
```
### `setup(config)`
Configures reusable machine logic with strong typing.
```typescript
import { setup } from 'xstate';
const machine = setup({
types: {
context: {} as ContextType,
events: {} as EventType,
input: {} as InputType,
output: {} as OutputType,
},
actions: {
// Named action implementations
actionName: (context, event, meta) => {
/* ... */
},
},
guards: {
// Named guard implementations
guardName: (context, event, meta) => boolean,
},
actors: {
// Actor logic definitions
actorName: fromPromise(() => fetch('/api')),
},
delays: {
// Named delay functions
delayName: (context, event) => 1000,
},
}).createMachine({
// Machine configuration
});
```
## Actor Creation and Management
### `createActor(logic, options?)`
Creates an actor instance from machine or actor logic.
```typescript
const actor = createActor(machine, {
id: 'actorId', // Actor identifier
input: {}, // Initial input
snapshot: {}, // Restored snapshot
systemId: 'systemId', // Actor system ID
logger: console.log, // Custom logger
inspect: (event) => {}, // Inspection handler
});
```
### Actor Methods
```typescript
// Lifecycle
actor.start(); // Start the actor
actor.stop(); // Stop the actor
actor.getSnapshot(); // Get current snapshot
actor.getPersistedSnapshot(); // Get persistable snapshot
// Communication
actor.send(event); // Send an event
actor.send({ type: 'EVENT' }); // Send event object
// Subscription
const subscription = actor.subscribe(observer);
subscription.unsubscribe();
// System
actor.system; // Access actor system
```
## State Configuration
### State Node Properties
```typescript
{
type: 'atomic' | 'compound' | 'parallel' | 'final' | 'history',
initial: 'childStateName', // For compound states
context: {}, // State-specific context
states: {}, // Child states
on: {}, // Transitions
entry: [], // Entry actions
exit: [], // Exit actions
always: [], // Eventless transitions
after: {}, // Delayed transitions
invoke: {}, // Invoked services
tags: [], // State tags
description: '', // State description
meta: {}, // Metadata
history: 'shallow' | 'deep', // For history states
}
```
### Transition Configuration
```typescript
{
target: 'stateName' | ['state1', 'state2'], // Target state(s)
guard: 'guardName' | guardFunction, // Condition
actions: ['action1', 'action2'], // Actions to execute
reenter: boolean, // Re-enter state
description: 'Transition description', // Documentation
}
```
## Actions
### `assign(assignment)`
Updates machine context immutably.
```typescript
// Function updater
assign({
count: ({ context }) => context.count + 1,
user: ({ context, event }) => event.user,
});
// Object updater
assign({ count: 5, user: null });
// Single property
assign(({ context }) => ({
...context,
modified: true,
}));
```
### `raise(event)`
Raises an event internally.
```typescript
raise({ type: 'INTERNAL_EVENT' });
raise(({ context }) => ({
type: 'DYNAMIC_EVENT',
data: context.someValue,
}));
```
### `sendTo(actor, event)`
Sends event to another actor.
```typescript
sendTo('actorId', { type: 'MESSAGE' });
sendTo(({ context }) => context.someActor, { type: 'EVENT' });
```
### `emit(event)`
Emits an event to parent actor.
```typescript
emit({ type: 'CHILD_EVENT', data: value });
```
### `log(message)`
Logs a message (for debugging).
```typescript
log('State entered');
log(({ context }) => `Count: ${context.count}`);
```
### `stop(actorId)`
Stops a spawned actor.
```typescript
stop('childActorId');
```
### `cancel(sendId)`
Cancels a delayed send.
```typescript
cancel('delayedSendId');
```
### `enqueueActions(callback)`
Enqueues actions conditionally at runtime. Replaces v4's `pure()` and `choose()`.
```typescript
import { enqueueActions } from 'xstate';
// Basic conditional actions
entry: enqueueActions(({ context, event, enqueue, check }) => {
// Conditionally enqueue actions
if (context.count > 0) {
enqueue('logPositive');
}
// Use check() for guards
if (check({ type: 'hasPermission' })) {
enqueue('performSecureAction');
}
// Enqueue multiple actions
enqueue([
{ type: 'log', params: { message: 'Processing' } },
'processData',
assign({ processing: true }),
]);
});
// With parameters
enqueueActions(({ context, enqueue }, params: { threshold: number }) => {
if (context.value > params.threshold) {
enqueue('handleHighValue');
}
});
```
## Guards
### Guard Functions
```typescript
// Inline guard
guard: ({ context, event }) => context.count > 0;
// Named guard
guard: 'isValid';
// Negated guard
guard: not('isInvalid');
// Combined guards
guard: and(['isValid', 'isAuthorized']);
guard: or(['isAdmin', 'isOwner']);
```
### Guard Helpers
```typescript
import { not, and, or, stateIn } from 'xstate';
not(guard); // Negates a guard
and([guard1, guard2]); // All must be true
or([guard1, guard2]); // At least one must be true
stateIn('state.path'); // Checks if in state
```
## Invoked Services
### Service Configuration
```typescript
invoke: {
id: 'serviceId',
src: 'serviceName' | actorLogic,
input: ({ context, event }) => ({}),
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
onSnapshot: {
actions: ({ event }) => console.log(event.snapshot),
},
}
```
### Multiple Invocations
```typescript
invoke: [
{ id: 'service1', src: 'api1' },
{ id: 'service2', src: 'api2' },
];
```
## Spawning Actors
### `spawn(logic, options?)`
Spawns a child actor.
```typescript
import { spawn } from 'xstate';
// In an action
spawn(childMachine, {
id: 'childId',
systemId: 'childSystem',
input: { initial: 'data' },
syncSnapshot: true,
});
```
### `stopChild(actorId)`
Stops a spawned child actor.
```typescript
stopChild('childId');
```
## Delayed Transitions
### After Configuration
```typescript
after: {
1000: 'timeout', // Fixed delay
DELAY_NAME: 'delayed', // Named delay
[({ context }) => context.delay]: 'dynamic', // Dynamic delay
}
```
## Eventless Transitions
### Always Configuration
```typescript
always: [
{ target: 'state1', guard: 'condition1' },
{ target: 'state2', guard: 'condition2' },
{ target: 'default' }, // Fallback
];
```
## Utility Functions
### `waitFor(actor, predicate, options?)`
Waits for an actor to reach a specific condition.
```typescript
const snapshot = await waitFor(actor, (snapshot) => snapshot.matches('done'), {
timeout: 5000,
});
```
### `toPromise(actor)`
Converts an actor to a Promise.
```typescript
const result = await toPromise(actor);
```
### `createEmptyActor()`
Creates an actor that does nothing.
```typescript
const emptyActor = createEmptyActor();
```
## Type Helpers
### `ActorRefFrom<T>`
Gets the ActorRef type from logic.
```typescript
type MyActorRef = ActorRefFrom<typeof machine>;
```
### `SnapshotFrom<T>`
Gets the Snapshot type from logic or ActorRef.
```typescript
type MySnapshot = SnapshotFrom<typeof machine>;
```
### `EventFromLogic<T>`
Gets the event union type from logic.
```typescript
type MyEvents = EventFromLogic<typeof machine>;
```
### `StateValueFrom<T>`
Gets the state value type from logic.
```typescript
type MyStateValue = StateValueFrom<typeof machine>;
```
### `ContextFrom<T>`
Gets the context type from logic.
```typescript
type MyContext = ContextFrom<typeof machine>;
```
## Inspection and Debugging
### Inspector API
```typescript
const actor = createActor(machine, {
inspect: (inspectionEvent) => {
if (inspectionEvent.type === '@xstate.snapshot') {
console.log('Snapshot:', inspectionEvent.snapshot);
}
},
});
```
### Inspection Event Types
- `@xstate.actor` - Actor created/stopped
- `@xstate.snapshot` - Snapshot updated
- `@xstate.event` - Event processed
- `@xstate.microstep` - Microstep taken
## State Methods
### `state.matches(stateValue)`
Checks if in a specific state.
```typescript
if (state.matches('loading')) {
/* ... */
}
if (state.matches({ form: 'valid' })) {
/* ... */
}
```
### `state.hasTag(tag)`
Checks if state has a tag.
```typescript
if (state.hasTag('loading')) {
/* ... */
}
```
### `state.can(event)`
Checks if an event can cause a transition.
```typescript
if (state.can({ type: 'SUBMIT' })) {
/* ... */
}
```
### `state.nextEvents`
Gets available events from current state.
```typescript
const events = state.nextEvents; // ['SUBMIT', 'CANCEL']
```

View File

@@ -0,0 +1,929 @@
# XState v5 Common Patterns
## Conditional Actions Pattern
### Using enqueueActions (replaces v4's pure/choose)
```typescript
const machine = createMachine({
context: {
count: 0,
user: null,
isAdmin: false,
},
on: {
PROCESS: {
actions: enqueueActions(({ context, event, enqueue, check }) => {
// Conditional logic at runtime
if (context.count > 10) {
enqueue('notifyHighCount');
}
// Check guards
if (check('isAuthenticated')) {
enqueue('processAuthenticatedUser');
if (context.isAdmin) {
enqueue('grantAdminPrivileges');
}
} else {
enqueue('redirectToLogin');
}
// Dynamic action selection
const action = context.count % 2 === 0 ? 'handleEven' : 'handleOdd';
enqueue(action);
// Always executed
enqueue(assign({ lastProcessed: Date.now() }));
}),
},
},
});
```
## Loading States Pattern
### Basic Loading Pattern
```typescript
const fetchMachine = createMachine({
initial: 'idle',
context: {
data: null,
error: null,
},
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
entry: assign({ error: null }), // Clear previous errors
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({
data: ({ event }) => event.output,
}),
},
onError: {
target: 'failure',
actions: assign({
error: ({ event }) => event.error,
}),
},
},
},
success: {
on: {
REFETCH: 'loading',
},
},
failure: {
on: {
RETRY: 'loading',
},
},
},
});
```
### With Retry Logic
```typescript
const retryMachine = createMachine({
context: {
retries: 0,
maxRetries: 3,
data: null,
error: null,
},
states: {
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({
data: ({ event }) => event.output,
retries: 0, // Reset on success
}),
},
onError: [
{
target: 'retrying',
guard: ({ context }) => context.retries < context.maxRetries,
actions: assign({
retries: ({ context }) => context.retries + 1,
}),
},
{
target: 'failure',
actions: assign({
error: ({ event }) => event.error,
}),
},
],
},
},
retrying: {
after: {
1000: 'loading', // Retry after 1 second
},
},
success: {},
failure: {},
},
});
```
## Form Validation Pattern
### Multi-field Form Validation
```typescript
const formMachine = setup({
types: {
context: {} as {
fields: {
email: string;
password: string;
};
errors: {
email?: string;
password?: string;
};
touched: {
email: boolean;
password: boolean;
};
},
},
guards: {
isEmailValid: ({ context }) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(context.fields.email),
isPasswordValid: ({ context }) => context.fields.password.length >= 8,
isFormValid: ({ context }) =>
!context.errors.email && !context.errors.password,
},
}).createMachine({
initial: 'editing',
context: {
fields: { email: '', password: '' },
errors: {},
touched: { email: false, password: false },
},
states: {
editing: {
on: {
UPDATE_EMAIL: {
actions: [
assign({
fields: ({ context, event }) => ({
...context.fields,
email: event.value,
}),
touched: ({ context }) => ({
...context.touched,
email: true,
}),
}),
'validateEmail',
],
},
UPDATE_PASSWORD: {
actions: [
assign({
fields: ({ context, event }) => ({
...context.fields,
password: event.value,
}),
touched: ({ context }) => ({
...context.touched,
password: true,
}),
}),
'validatePassword',
],
},
SUBMIT: {
target: 'validating',
},
},
},
validating: {
always: [
{
target: 'submitting',
guard: 'isFormValid',
},
{
target: 'editing',
actions: assign({
touched: { email: true, password: true },
}),
},
],
},
submitting: {
invoke: {
src: 'submitForm',
input: ({ context }) => context.fields,
onDone: {
target: 'success',
},
onError: {
target: 'editing',
actions: assign({
errors: ({ event }) => event.error.fieldErrors || {},
}),
},
},
},
success: {
type: 'final',
},
},
});
```
## Authentication Flow Pattern
```typescript
const authMachine = createMachine({
initial: 'checkingAuth',
context: {
user: null,
token: null,
},
states: {
checkingAuth: {
invoke: {
src: 'checkStoredAuth',
onDone: [
{
target: 'authenticated',
guard: ({ event }) => !!event.output.token,
actions: assign({
user: ({ event }) => event.output.user,
token: ({ event }) => event.output.token,
}),
},
{
target: 'unauthenticated',
},
],
},
},
unauthenticated: {
on: {
LOGIN: 'authenticating',
REGISTER: 'registering',
},
},
authenticating: {
invoke: {
src: 'authenticate',
input: ({ event }) => ({
email: event.email,
password: event.password,
}),
onDone: {
target: 'authenticated',
actions: [
assign({
user: ({ event }) => event.output.user,
token: ({ event }) => event.output.token,
}),
'storeAuth',
],
},
onError: {
target: 'unauthenticated',
actions: 'showError',
},
},
},
registering: {
// Similar to authenticating
},
authenticated: {
on: {
LOGOUT: {
target: 'unauthenticated',
actions: [assign({ user: null, token: null }), 'clearStoredAuth'],
},
TOKEN_EXPIRED: 'refreshing',
},
},
refreshing: {
invoke: {
src: 'refreshToken',
onDone: {
target: 'authenticated',
actions: assign({
token: ({ event }) => event.output.token,
}),
},
onError: {
target: 'unauthenticated',
actions: ['clearStoredAuth'],
},
},
},
},
});
```
## Pagination Pattern
```typescript
const paginationMachine = createMachine({
context: {
items: [],
currentPage: 1,
totalPages: 0,
pageSize: 10,
totalItems: 0,
isLoading: false,
},
initial: 'idle',
states: {
idle: {
on: {
LOAD_PAGE: {
target: 'loading',
},
},
},
loading: {
entry: assign({ isLoading: true }),
exit: assign({ isLoading: false }),
invoke: {
src: 'fetchPage',
input: ({ context, event }) => ({
page: event.page || context.currentPage,
pageSize: context.pageSize,
}),
onDone: {
target: 'idle',
actions: assign({
items: ({ event }) => event.output.items,
currentPage: ({ event }) => event.output.page,
totalPages: ({ event }) => event.output.totalPages,
totalItems: ({ event }) => event.output.totalItems,
}),
},
onError: {
target: 'error',
},
},
},
error: {
on: {
RETRY: 'loading',
},
},
},
on: {
NEXT_PAGE: {
target: '.loading',
guard: ({ context }) => context.currentPage < context.totalPages,
actions: assign({
currentPage: ({ context }) => context.currentPage + 1,
}),
},
PREV_PAGE: {
target: '.loading',
guard: ({ context }) => context.currentPage > 1,
actions: assign({
currentPage: ({ context }) => context.currentPage - 1,
}),
},
GO_TO_PAGE: {
target: '.loading',
guard: ({ context, event }) =>
event.page > 0 && event.page <= context.totalPages,
actions: assign({
currentPage: ({ event }) => event.page,
}),
},
},
});
```
## Wizard/Stepper Pattern
```typescript
const wizardMachine = createMachine({
initial: 'step1',
context: {
step1Data: null,
step2Data: null,
step3Data: null,
},
states: {
step1: {
initial: 'editing',
states: {
editing: {
on: {
SAVE: {
target: 'validated',
actions: assign({
step1Data: ({ event }) => event.data,
}),
},
},
},
validated: {
type: 'final',
},
},
onDone: {
target: 'step2',
},
},
step2: {
initial: 'editing',
states: {
editing: {
on: {
SAVE: {
target: 'validated',
actions: assign({
step2Data: ({ event }) => event.data,
}),
},
},
},
validated: {
type: 'final',
},
},
on: {
BACK: 'step1',
},
onDone: {
target: 'step3',
},
},
step3: {
initial: 'editing',
states: {
editing: {
on: {
SAVE: {
target: 'validated',
actions: assign({
step3Data: ({ event }) => event.data,
}),
},
},
},
validated: {
type: 'final',
},
},
on: {
BACK: 'step2',
},
onDone: {
target: 'review',
},
},
review: {
on: {
EDIT_STEP1: 'step1',
EDIT_STEP2: 'step2',
EDIT_STEP3: 'step3',
SUBMIT: 'submitting',
},
},
submitting: {
invoke: {
src: 'submitWizard',
input: ({ context }) => ({
step1: context.step1Data,
step2: context.step2Data,
step3: context.step3Data,
}),
onDone: {
target: 'complete',
},
onError: {
target: 'review',
actions: 'showError',
},
},
},
complete: {
type: 'final',
},
},
});
```
## Parallel States Pattern
### Upload/Download Manager
```typescript
const fileManagerMachine = createMachine({
type: 'parallel',
context: {
uploads: [],
downloads: [],
},
states: {
upload: {
initial: 'idle',
states: {
idle: {
on: {
START_UPLOAD: 'uploading',
},
},
uploading: {
invoke: {
src: 'uploadFiles',
onDone: {
target: 'idle',
actions: 'addUploadedFiles',
},
onError: {
target: 'uploadError',
},
},
},
uploadError: {
on: {
RETRY_UPLOAD: 'uploading',
CANCEL_UPLOAD: 'idle',
},
},
},
},
download: {
initial: 'idle',
states: {
idle: {
on: {
START_DOWNLOAD: 'downloading',
},
},
downloading: {
invoke: {
src: 'downloadFiles',
onDone: {
target: 'idle',
actions: 'addDownloadedFiles',
},
onError: {
target: 'downloadError',
},
},
},
downloadError: {
on: {
RETRY_DOWNLOAD: 'downloading',
CANCEL_DOWNLOAD: 'idle',
},
},
},
},
},
});
```
## History States Pattern
### Editor with History
```typescript
const editorMachine = createMachine({
initial: 'editing',
context: {
content: '',
mode: 'text',
},
states: {
editing: {
initial: 'text',
states: {
text: {
on: {
SWITCH_TO_VISUAL: 'visual',
},
},
visual: {
on: {
SWITCH_TO_TEXT: 'text',
OPEN_SETTINGS: '#editor.settings',
},
},
history: {
type: 'history',
history: 'shallow',
},
},
on: {
SAVE: 'saving',
},
},
settings: {
on: {
CLOSE: '#editor.editing.history', // Return to previous state
APPLY: {
target: '#editor.editing.history',
actions: 'applySettings',
},
},
},
saving: {
invoke: {
src: 'saveContent',
onDone: {
target: 'editing',
},
onError: {
target: 'editing',
actions: 'showSaveError',
},
},
},
},
});
```
## Debouncing Pattern
### Search with Debounce
```typescript
const searchMachine = createMachine({
initial: 'idle',
context: {
query: '',
results: [],
},
states: {
idle: {
on: {
SEARCH: {
target: 'debouncing',
actions: assign({
query: ({ event }) => event.query,
}),
},
},
},
debouncing: {
after: {
300: 'searching', // 300ms debounce
},
on: {
SEARCH: {
target: 'debouncing',
actions: assign({
query: ({ event }) => event.query,
}),
reenter: true, // Reset the timer
},
},
},
searching: {
invoke: {
src: 'performSearch',
input: ({ context }) => ({ query: context.query }),
onDone: {
target: 'idle',
actions: assign({
results: ({ event }) => event.output,
}),
},
onError: {
target: 'idle',
actions: 'logError',
},
},
},
},
});
```
## Queue Processing Pattern
```typescript
const queueMachine = createMachine({
context: {
queue: [],
currentItem: null,
processed: [],
failed: [],
},
initial: 'idle',
states: {
idle: {
always: [
{
target: 'processing',
guard: ({ context }) => context.queue.length > 0,
},
],
on: {
ADD_TO_QUEUE: {
actions: assign({
queue: ({ context, event }) => [...context.queue, event.item],
}),
target: 'processing',
},
},
},
processing: {
entry: assign({
currentItem: ({ context }) => context.queue[0],
queue: ({ context }) => context.queue.slice(1),
}),
invoke: {
src: 'processItem',
input: ({ context }) => context.currentItem,
onDone: {
target: 'idle',
actions: assign({
processed: ({ context, event }) => [
...context.processed,
{ item: context.currentItem, result: event.output },
],
currentItem: null,
}),
},
onError: {
target: 'idle',
actions: assign({
failed: ({ context, event }) => [
...context.failed,
{ item: context.currentItem, error: event.error },
],
currentItem: null,
}),
},
},
},
},
on: {
CLEAR_QUEUE: {
actions: assign({
queue: [],
processed: [],
failed: [],
}),
},
},
});
```
## Modal/Dialog Pattern
```typescript
const modalMachine = createMachine({
initial: 'closed',
context: {
data: null,
result: null,
},
states: {
closed: {
on: {
OPEN: {
target: 'open',
actions: assign({
data: ({ event }) => event.data,
}),
},
},
},
open: {
initial: 'idle',
states: {
idle: {
on: {
CONFIRM: {
target: 'confirming',
},
},
},
confirming: {
invoke: {
src: 'handleConfirm',
input: ({ context }) => context.data,
onDone: {
actions: [
assign({
result: ({ event }) => event.output,
}),
emit({ type: 'MODAL_CONFIRMED' }),
],
target: '#modal.closed',
},
onError: {
target: 'idle',
actions: 'showError',
},
},
},
},
on: {
CANCEL: {
target: 'closed',
actions: [
assign({ data: null, result: null }),
emit({ type: 'MODAL_CANCELLED' }),
],
},
CLOSE: {
target: 'closed',
actions: assign({ data: null, result: null }),
},
},
},
},
});
```
## Connection Management Pattern
```typescript
const connectionMachine = createMachine({
initial: 'disconnected',
context: {
retries: 0,
maxRetries: 5,
socket: null,
},
states: {
disconnected: {
on: {
CONNECT: 'connecting',
},
},
connecting: {
invoke: {
src: 'createConnection',
onDone: {
target: 'connected',
actions: assign({
socket: ({ event }) => event.output,
retries: 0,
}),
},
onError: [
{
target: 'reconnecting',
guard: ({ context }) => context.retries < context.maxRetries,
actions: assign({
retries: ({ context }) => context.retries + 1,
}),
},
{
target: 'failed',
},
],
},
},
connected: {
invoke: {
src: 'monitorConnection',
onError: {
target: 'reconnecting',
},
},
on: {
DISCONNECT: {
target: 'disconnected',
actions: 'closeConnection',
},
CONNECTION_LOST: 'reconnecting',
},
},
reconnecting: {
after: {
// Exponential backoff
[({ context }) => Math.min(1000 * Math.pow(2, context.retries), 30000)]:
'connecting',
},
},
failed: {
on: {
RETRY: {
target: 'connecting',
actions: assign({ retries: 0 }),
},
},
},
},
});
```

View File

@@ -0,0 +1,491 @@
# XState v5 Testing Reference (Backend Focus)
This reference covers testing XState v5 actors and state machines for **backend applications**. For comprehensive actor testing patterns, see the [xstate-audition skill](../../xstate-audition/SKILL.md).
## Testing Strategy Overview
XState v5 testing for backend applications focuses on:
- Actor lifecycle and behavior testing
- State transition verification
- Event handling and inter-actor communication
- Asynchronous operation testing
- Error handling and recovery
## Recommended Testing Library: xstate-audition
For comprehensive actor testing, use **[xstate-audition](https://boneskull.github.io/xstate-audition/)**, which provides:
- Dependency-free XState v5 actor testing
- Async-first testing utilities
- Comprehensive actor lifecycle testing
- Built-in timeout handling
- Curried functions for test reusability
See the [xstate-audition skill documentation](../../xstate-audition/SKILL.md) for detailed usage patterns.
## Basic Unit Testing
While xstate-audition excels at actor testing, you may need to unit test individual pieces:
### Testing Guards in Isolation
```typescript
import { describe, test, expect } from 'vitest';
// Guard function
const isValidThreshold = ({ context }: { context: { value: number } }) =>
context.value > 10;
describe('guards', () => {
test('isValidThreshold returns true for values > 10', () => {
expect(isValidThreshold({ context: { value: 15 } })).toBe(true);
expect(isValidThreshold({ context: { value: 5 } })).toBe(false);
});
});
```
### Testing Action Functions
```typescript
import { vi, describe, test, expect } from 'vitest';
// Action function
const logMessage = ({ context, event }: any) => {
console.log(`Event: ${event.type}, Count: ${context.count}`);
};
test('logMessage action logs correctly', () => {
const consoleSpy = vi.spyOn(console, 'log');
logMessage({
context: { count: 5 },
event: { type: 'TEST_EVENT' },
});
expect(consoleSpy).toHaveBeenCalledWith('Event: TEST_EVENT, Count: 5');
});
```
## Backend-Specific Testing Patterns
### Testing Database Transaction States
```typescript
import { createActor } from 'xstate';
import { runUntilDone, waitForSnapshot } from 'xstate-audition';
const dbTransactionMachine = createMachine({
initial: 'idle',
states: {
idle: {
on: { BEGIN: 'transaction' },
},
transaction: {
initial: 'processing',
states: {
processing: {
invoke: {
src: 'executeQuery',
onDone: 'committing',
onError: 'rollingBack',
},
},
committing: {
invoke: {
src: 'commitTransaction',
onDone: '#db.success',
onError: '.rollingBack',
},
},
rollingBack: {
invoke: {
src: 'rollbackTransaction',
onDone: '#db.failed',
},
},
},
},
success: { type: 'final' },
failed: { type: 'final' },
},
});
test('successful database transaction', async () => {
const actor = createActor(dbTransactionMachine);
const promise = waitForSnapshot(actor, (s) => s.matches('success'));
actor.send({ type: 'BEGIN' });
await promise;
expect(actor.getSnapshot().value).toBe('success');
});
```
### Testing Message Queue Processing
```typescript
const queueProcessorMachine = createMachine({
context: {
messages: [],
processed: 0,
failed: 0,
},
initial: 'polling',
states: {
polling: {
invoke: {
src: 'pollQueue',
onDone: {
target: 'processing',
actions: assign({
messages: ({ event }) => event.output,
}),
},
},
},
processing: {
always: [
{
target: 'idle',
guard: ({ context }) => context.messages.length === 0,
},
{
target: 'processingMessage',
},
],
},
processingMessage: {
invoke: {
src: 'processMessage',
input: ({ context }) => context.messages[0],
onDone: {
target: 'processing',
actions: assign({
messages: ({ context }) => context.messages.slice(1),
processed: ({ context }) => context.processed + 1,
}),
},
onError: {
target: 'processing',
actions: assign({
messages: ({ context }) => context.messages.slice(1),
failed: ({ context }) => context.failed + 1,
}),
},
},
},
idle: {
after: {
5000: 'polling',
},
},
},
});
test('processes queue messages', async () => {
const actor = createActor(queueProcessorMachine);
await waitForSnapshot(actor, (snapshot) => snapshot.context.processed >= 3);
expect(actor.getSnapshot().context.failed).toBe(0);
});
```
### Testing API Rate Limiting States
```typescript
const rateLimiterMachine = createMachine({
context: {
requestCount: 0,
resetTime: null,
},
initial: 'ready',
states: {
ready: {
on: {
REQUEST: [
{
target: 'throttled',
guard: ({ context }) => context.requestCount >= 100,
actions: assign({
resetTime: () => Date.now() + 60000,
}),
},
{
target: 'processing',
actions: assign({
requestCount: ({ context }) => context.requestCount + 1,
}),
},
],
},
},
processing: {
invoke: {
src: 'handleRequest',
onDone: 'ready',
onError: 'ready',
},
},
throttled: {
after: {
60000: {
target: 'ready',
actions: assign({
requestCount: 0,
resetTime: null,
}),
},
},
},
},
});
test('enforces rate limits', async () => {
const actor = createActor(rateLimiterMachine);
// Send 100 requests
for (let i = 0; i < 100; i++) {
actor.send({ type: 'REQUEST' });
}
// 101st request should be throttled
actor.send({ type: 'REQUEST' });
await waitForSnapshot(actor, (s) => s.matches('throttled'));
expect(actor.getSnapshot().context.resetTime).toBeDefined();
});
```
## Testing with Mock Services
### Mocking External Services
```typescript
import { vi } from 'vitest';
import { fromPromise } from 'xstate';
test('handles service failures gracefully', async () => {
const mockService = vi
.fn()
.mockRejectedValue(new Error('Service unavailable'));
const machine = setup({
actors: {
externalService: fromPromise(mockService),
},
}).createMachine({
initial: 'calling',
states: {
calling: {
invoke: {
src: 'externalService',
onError: {
target: 'fallback',
actions: assign({
error: ({ event }) => event.error.message,
}),
},
},
},
fallback: {
type: 'final',
},
},
});
const actor = createActor(machine);
const result = await runUntilDone(actor);
expect(actor.getSnapshot().context.error).toBe('Service unavailable');
expect(mockService).toHaveBeenCalledTimes(1);
});
```
## Testing Async Behavior
### Using waitFor from xstate
```typescript
import { waitFor } from 'xstate';
test('waits for async completion', async () => {
const actor = createActor(asyncMachine);
actor.start();
actor.send({ type: 'START_ASYNC' });
const snapshot = await waitFor(
actor,
(snapshot) => snapshot.context.isComplete === true,
{ timeout: 5000 },
);
expect(snapshot.context.result).toBeDefined();
});
```
### Testing with Fake Timers
```typescript
import { vi, beforeEach, afterEach } from 'vitest';
describe('delayed transitions', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test('transitions after delay', () => {
const actor = createActor(delayedMachine);
actor.start();
expect(actor.getSnapshot().value).toBe('waiting');
vi.advanceTimersByTime(5000);
expect(actor.getSnapshot().value).toBe('ready');
});
});
```
## Testing Actor Communication
### Testing Parent-Child Actors
```typescript
import { spawn, sendTo } from 'xstate';
import { runUntilSpawn, waitForSnapshot } from 'xstate-audition';
test('parent spawns and communicates with child', async () => {
const parentMachine = createMachine({
initial: 'idle',
context: {
childResponse: null,
},
states: {
idle: {
on: {
START: {
actions: spawn(childMachine, { id: 'child' }),
target: 'waiting',
},
},
},
waiting: {
on: {
CHILD_RESPONSE: {
actions: assign({
childResponse: ({ event }) => event.data,
}),
target: 'done',
},
},
entry: sendTo('child', { type: 'PROCESS' }),
},
done: { type: 'final' },
},
});
const actor = createActor(parentMachine);
const childRef = await runUntilSpawn(actor, 'child');
await waitForSnapshot(actor, (s) => s.matches('done'));
expect(actor.getSnapshot().context.childResponse).toBeDefined();
});
```
## Performance Testing
```typescript
test('handles high throughput', async () => {
const start = performance.now();
const actor = createActor(highThroughputMachine);
actor.start();
// Send many events rapidly
for (let i = 0; i < 10000; i++) {
actor.send({ type: 'PROCESS', data: i });
}
await waitFor(actor, (s) => s.context.processedCount === 10000);
const duration = performance.now() - start;
expect(duration).toBeLessThan(5000); // Should process within 5 seconds
});
```
## Error Handling Tests
```typescript
test('recovers from errors with retry', async () => {
let attempts = 0;
const mockService = vi.fn().mockImplementation(() => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary failure');
}
return { success: true };
});
const retryMachine = createMachine({
context: { retries: 0, maxRetries: 3 },
initial: 'attempting',
states: {
attempting: {
invoke: {
src: fromPromise(mockService),
onDone: 'success',
onError: [
{
target: 'attempting',
guard: ({ context }) => context.retries < context.maxRetries,
actions: assign({
retries: ({ context }) => context.retries + 1,
}),
},
{
target: 'failed',
},
],
},
},
success: { type: 'final' },
failed: { type: 'final' },
},
});
const actor = createActor(retryMachine);
await runUntilDone(actor);
expect(actor.getSnapshot().value).toBe('success');
expect(mockService).toHaveBeenCalledTimes(3);
});
```
## Test Organization Best Practices
1. **Separate unit tests from integration tests** - Test guards and actions separately from full actor behavior
2. **Use xstate-audition for actor tests** - Leverage its powerful async utilities
3. **Mock external dependencies** - Database, APIs, message queues
4. **Test error paths explicitly** - Ensure graceful degradation
5. **Use descriptive test names** - Clearly indicate what behavior is being tested
6. **Keep tests focused** - One behavior per test
7. **Use fake timers for time-dependent logic** - Control time progression explicitly
## Summary
For backend XState v5 testing:
- Use **[xstate-audition](../../xstate-audition/SKILL.md)** for comprehensive actor testing
- Focus on actor behavior, not UI interactions
- Test database transactions, message processing, and API interactions
- Mock external services appropriately
- Handle async behavior with proper utilities
- Ensure error recovery and retry logic works correctly

View File

@@ -0,0 +1,626 @@
# XState v5 TypeScript Reference
## Requirements
- **TypeScript 5.0 or greater** is required for XState v5
- Enable `strictNullChecks: true` in tsconfig.json (strongly recommended)
- Set `skipLibCheck: true` for better performance
## Setup Pattern (Recommended)
The `setup()` function is the primary way to achieve strong typing in XState v5.
### Basic Setup
```typescript
import { setup } from 'xstate';
const machine = setup({
types: {
context: {} as {
count: number;
user: User | null;
},
events: {} as
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_USER'; user: User },
input: {} as {
initialCount?: number;
},
output: {} as {
finalCount: number;
},
},
actions: {
incrementCount: assign({
count: ({ context }) => context.count + 1,
}),
setUser: assign({
user: ({ event }) => {
// TypeScript knows event has 'user' property here
assertEvent(event, 'SET_USER');
return event.user;
},
}),
},
guards: {
isPositive: ({ context }) => context.count > 0,
hasUser: ({ context }) => context.user !== null,
},
actors: {
fetchUser: fromPromise(async ({ input }: { input: { id: string } }) => {
const response = await fetch(`/api/users/${input.id}`);
return response.json() as Promise<User>;
}),
},
delays: {
RETRY_DELAY: ({ context }) => context.count * 1000,
},
}).createMachine({
// Machine configuration with full type inference
id: 'typedMachine',
initial: 'idle',
context: ({ input }) => ({
count: input?.initialCount ?? 0,
user: null,
}),
states: {
idle: {
on: {
INCREMENT: {
actions: 'incrementCount',
},
SET_USER: {
actions: 'setUser',
},
},
},
},
});
```
## Type Inference
### Context Type Inference
```typescript
// Context is fully typed in actions, guards, etc.
const machine = setup({
types: {
context: {} as {
items: string[];
selectedIndex: number;
},
},
}).createMachine({
context: {
items: [],
selectedIndex: -1,
},
// TypeScript knows context shape everywhere
entry: ({ context }) => {
console.log(context.items); // string[]
console.log(context.selectedIndex); // number
},
});
```
### Event Type Inference
```typescript
const machine = setup({
types: {
events: {} as
| { type: 'ADD_ITEM'; item: string }
| { type: 'REMOVE_ITEM'; index: number }
| { type: 'CLEAR_ALL' },
},
}).createMachine({
on: {
ADD_ITEM: {
actions: ({ event }) => {
// TypeScript knows event.item exists and is string
console.log(event.item.toUpperCase());
},
},
REMOVE_ITEM: {
actions: ({ event }) => {
// TypeScript knows event.index exists and is number
console.log(event.index + 1);
},
},
},
});
```
## Dynamic Parameters
### Typed Action Parameters
```typescript
const machine = setup({
types: {
context: {} as { message: string },
},
actions: {
log: (
{ context },
params: { prefix: string; level?: 'info' | 'warn' | 'error' },
) => {
const level = params.level || 'info';
console[level](`${params.prefix}: ${context.message}`);
},
},
}).createMachine({
entry: {
type: 'log',
params: { prefix: 'Starting', level: 'info' },
},
});
```
### Typed Guard Parameters
```typescript
const machine = setup({
types: {
context: {} as { value: number },
},
guards: {
isGreaterThan: ({ context }, params: { threshold: number }) =>
context.value > params.threshold,
isInRange: ({ context }, params: { min: number; max: number }) =>
context.value >= params.min && context.value <= params.max,
},
}).createMachine({
on: {
CHECK: [
{
target: 'high',
guard: {
type: 'isGreaterThan',
params: { threshold: 100 },
},
},
{
target: 'medium',
guard: {
type: 'isInRange',
params: { min: 50, max: 100 },
},
},
],
},
});
```
## Event Assertion
Use `assertEvent()` to narrow event types:
```typescript
import { assertEvent } from 'xstate';
const machine = setup({
types: {
events: {} as
| { type: 'SUBMIT'; data: FormData }
| { type: 'CANCEL' }
| { type: 'RESET'; fields?: string[] },
},
}).createMachine({
on: {
'*': {
actions: ({ event }) => {
// Assert single event type
assertEvent(event, 'SUBMIT');
console.log(event.data); // TypeScript knows data exists
// Assert multiple event types
assertEvent(event, ['SUBMIT', 'RESET']);
if (event.type === 'SUBMIT') {
console.log(event.data);
} else {
console.log(event.fields);
}
},
},
},
});
```
## Type Helper Utilities
### ActorRefFrom
Extract actor reference type from machine or logic:
```typescript
import { ActorRefFrom } from 'xstate';
const machine = createMachine({
/* ... */
});
type MachineActorRef = ActorRefFrom<typeof machine>;
function handleActor(actor: MachineActorRef) {
actor.send({ type: 'SOME_EVENT' });
const snapshot = actor.getSnapshot();
}
```
### SnapshotFrom
Extract snapshot type:
```typescript
import { SnapshotFrom } from 'xstate';
type MachineSnapshot = SnapshotFrom<typeof machine>;
function processSnapshot(snapshot: MachineSnapshot) {
if (snapshot.matches('loading')) {
// Handle loading state
}
console.log(snapshot.context);
}
```
### EventFromLogic
Extract event union type:
```typescript
import { EventFromLogic } from 'xstate';
type MachineEvents = EventFromLogic<typeof machine>;
function createEvent(): MachineEvents {
return { type: 'INCREMENT' };
}
```
### StateValueFrom
Extract state value type:
```typescript
import { StateValueFrom } from 'xstate';
type MachineStateValue = StateValueFrom<typeof machine>;
function isInState(value: MachineStateValue) {
// Type-safe state value checking
}
```
### ContextFrom
Extract context type:
```typescript
import { ContextFrom } from 'xstate';
type MachineContext = ContextFrom<typeof machine>;
function processContext(context: MachineContext) {
// Access typed context properties
}
```
## Actor Types
### Typed Promise Actors
```typescript
interface UserData {
id: string;
name: string;
email: string;
}
const fetchUserLogic = fromPromise(
async ({ input }: { input: { userId: string } }): Promise<UserData> => {
const response = await fetch(`/api/users/${input.userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
);
// Use in machine
const machine = setup({
actors: {
fetchUser: fetchUserLogic,
},
}).createMachine({
invoke: {
src: 'fetchUser',
input: ({ context }) => ({ userId: context.userId }),
onDone: {
actions: assign({
user: ({ event }) => event.output, // Typed as UserData
}),
},
},
});
```
### Typed Callback Actors
```typescript
interface WebSocketMessage {
type: 'message' | 'error' | 'close';
data: any;
}
const websocketLogic = fromCallback<WebSocketMessage, { url: string }>(
({ sendBack, receive, input }) => {
const ws = new WebSocket(input.url);
ws.onmessage = (event) => {
sendBack({ type: 'message', data: event.data });
};
ws.onerror = (error) => {
sendBack({ type: 'error', data: error });
};
ws.onclose = () => {
sendBack({ type: 'close', data: null });
};
receive((event) => {
if (event.type === 'SEND') {
ws.send(event.data);
}
});
return () => ws.close();
},
);
```
## Generic Machines
### Creating Generic Machine Factories
```typescript
function createCrudMachine<T extends { id: string }>() {
return setup({
types: {
context: {} as {
items: T[];
selectedItem: T | null;
isLoading: boolean;
error: string | null;
},
events: {} as
| { type: 'FETCH' }
| { type: 'CREATE'; item: Omit<T, 'id'> }
| { type: 'UPDATE'; id: string; updates: Partial<T> }
| { type: 'DELETE'; id: string }
| { type: 'SELECT'; item: T },
},
}).createMachine({
initial: 'idle',
context: {
items: [],
selectedItem: null,
isLoading: false,
error: null,
},
states: {
idle: {
on: {
FETCH: 'fetching',
CREATE: 'creating',
SELECT: {
actions: assign({
selectedItem: ({ event }) => event.item,
}),
},
},
},
fetching: {
// Implementation
},
creating: {
// Implementation
},
},
});
}
// Use with specific type
interface User {
id: string;
name: string;
email: string;
}
const userMachine = createCrudMachine<User>();
```
## Discriminated Unions
### State-based Discrimination
```typescript
type MachineState =
| {
value: 'idle';
context: { data: null; error: null };
}
| {
value: 'loading';
context: { data: null; error: null };
}
| {
value: 'success';
context: { data: string; error: null };
}
| {
value: 'failure';
context: { data: null; error: Error };
};
const machine = setup({
types: {
context: {} as MachineState['context'],
},
}).createMachine({
initial: 'idle',
states: {
idle: {},
loading: {},
success: {},
failure: {},
},
});
// Type-safe state checking
function handleState(state: SnapshotFrom<typeof machine>) {
if (state.matches('success')) {
// TypeScript should know data is string here
console.log(state.context.data);
}
}
```
## Common TypeScript Patterns
### Exhaustive Event Handling
```typescript
const machine = setup({
types: {
events: {} as { type: 'A' } | { type: 'B' } | { type: 'C' },
},
}).createMachine({
on: {
'*': {
actions: ({ event }) => {
switch (event.type) {
case 'A':
// Handle A
break;
case 'B':
// Handle B
break;
case 'C':
// Handle C
break;
default:
// TypeScript ensures this is never reached
const exhaustive: never = event;
throw new Error(`Unhandled event: ${exhaustive}`);
}
},
},
},
});
```
### Branded Types for IDs
```typescript
type UserId = string & { __brand: 'UserId' };
type PostId = string & { __brand: 'PostId' };
const machine = setup({
types: {
context: {} as {
userId: UserId | null;
postId: PostId | null;
},
events: {} as
| { type: 'SET_USER'; id: UserId }
| { type: 'SET_POST'; id: PostId },
},
}).createMachine({
on: {
SET_USER: {
actions: assign({
userId: ({ event }) => event.id, // Type-safe: only UserId accepted
}),
},
SET_POST: {
actions: assign({
postId: ({ event }) => event.id, // Type-safe: only PostId accepted
}),
},
},
});
```
### Const Assertions for Events
```typescript
// Define events with const assertion for literal types
const EVENTS = {
INCREMENT: { type: 'INCREMENT' } as const,
DECREMENT: { type: 'DECREMENT' } as const,
RESET: { type: 'RESET' } as const,
SET_VALUE: (value: number) => ({ type: 'SET_VALUE', value }) as const,
} as const;
type MachineEvent = ReturnType<(typeof EVENTS)[keyof typeof EVENTS]>;
const machine = setup({
types: {
events: {} as MachineEvent,
},
}).createMachine({
// Machine configuration
});
// Usage
actor.send(EVENTS.INCREMENT);
actor.send(EVENTS.SET_VALUE(42));
```
## Migration from v4 Types
### Replacing Typegen
v4 used typegen for type safety. In v5, use `setup()` instead:
```typescript
// v4 with typegen
import { createMachine } from 'xstate';
import { typegen } from './machine.typegen';
const machine = createMachine({
tsTypes: typegen,
// ...
});
// v5 with setup
import { setup } from 'xstate';
const machine = setup({
types: {
context: {} as ContextType,
events: {} as EventsUnion,
},
}).createMachine({
// ...
});
```
## Best Practices
1. **Always use setup()** for new machines
2. **Define types first** in the setup configuration
3. **Use const assertions** for literal types
4. **Enable strict mode** in tsconfig.json
5. **Use branded types** for domain-specific IDs
6. **Leverage type helpers** (ActorRefFrom, SnapshotFrom, etc.)
7. **Assert events** when needed with assertEvent()
8. **Create generic factories** for reusable patterns
9. **Type external actors** properly with input/output types
10. **Use discriminated unions** for complex state variations