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

160
skills/xstate-v5/SKILL.md Normal file
View File

@@ -0,0 +1,160 @@
---
description: Expert guidance for implementing and analyzing XState v5 state machines with TypeScript support and best practices
triggers:
- xstate
- state machine
- statechart
- createMachine
- createActor
- fsm implementation
- actor model
- state management xstate
---
# XState v5 Expert Skill
You are an expert in XState v5, a JavaScript/TypeScript library for creating, interpreting, and executing finite state machines and statecharts using the actor model. Use this knowledge to help implement and analyze XState v5 code with precision and adherence to best practices.
## Core Concepts
### State Machines & Statecharts
XState implements event-driven programming through state machines and statecharts, providing predictable and robust logic handling. Always:
- Model application logic as explicit states and transitions
- Use statecharts for complex hierarchical and parallel state management
- Ensure every state transition is intentional and documented
### Actor Model
XState uses the actor model for distributed, concurrent computation:
- **State machine actors**: Primary actors created from state machines
- **Promise actors**: Handle asynchronous operations
- **Transition actors**: Manage pure transitions
- **Callback actors**: Custom imperative logic
- **Observable actors**: Stream-based actors
## Quick Start Example
```typescript
import { setup, createActor, assign } from 'xstate';
const machine = setup({
types: {
context: {} as { count: number },
events: {} as { type: 'INCREMENT' } | { type: 'DECREMENT' },
},
actions: {
increment: assign({ count: ({ context }) => context.count + 1 }),
decrement: assign({ count: ({ context }) => context.count - 1 }),
},
guards: {
isPositive: ({ context }) => context.count > 0,
},
}).createMachine({
id: 'counter',
initial: 'active',
context: { count: 0 },
states: {
active: {
on: {
INCREMENT: { actions: 'increment' },
DECREMENT: {
actions: 'decrement',
guard: 'isPositive',
},
},
},
},
});
const actor = createActor(machine);
actor.subscribe((snapshot) => console.log(snapshot.context));
actor.start();
actor.send({ type: 'INCREMENT' });
```
## 📚 Reference Documentation
For detailed implementation guidance, consult the comprehensive reference documentation:
### [Core API Reference](./references/core-api.md)
Complete API documentation including:
- Machine creation (`createMachine`, `setup`)
- Actor management and lifecycle
- Actions, guards, and services
- Utility functions and type helpers
### [Actors Reference](./references/actors.md)
Deep dive into the actor model:
- All actor types (state machine, promise, callback, transition, observable)
- Actor communication and orchestration
- Spawning vs invoking actors
- Error handling and persistence
### [Common Patterns](./references/patterns.md)
Production-ready patterns and solutions:
- Loading states with retry logic
- Form validation and submission
- Authentication flows
- Pagination, wizards, modals
- Debouncing and queue processing
### [TypeScript Integration](./references/typescript.md)
Complete TypeScript usage guide:
- Setup pattern with strong typing
- Type inference and helpers
- Generic machine factories
- Backend service types
### [Testing Strategies](./references/testing.md)
Backend testing approaches:
- Unit testing state machines
- Using xstate-audition for actor testing
- Mocking external services
- Testing async backend operations
## Best Practices
1. **Always use setup()** for better type inference and reusable logic
2. **Name all actions and guards** for clarity and reusability
3. **Use context for data, states for behavior**
4. **Keep machines focused** - one machine per logical unit
5. **Leverage TypeScript** for compile-time safety
6. **Avoid inline functions** in machine definitions. Used named guards and actions.
7. **Test with `xstate-audition`** for comprehensive coverage
8. **Use Promise actors** for asynchronous operations
## Common Mistakes to Avoid
1. **Don't mutate context directly** - always use `assign`
2. **Don't use side effects in guards** - guards should be pure
3. **Don't overuse nested states** - flatten when possible
4. **Don't ignore TypeScript errors** - they prevent runtime issues
5. **Don't mix concerns** - separate UI from business logic
6. **Don't use string events** when objects provide better typing
7. **Don't forget error handling** in async operations
8. **Don't use `setInterval`** in machine definitions. Use delays instead.
## Performance Tips
1. Use `enqueueActions()` for conditional actions instead of multiple transitions
2. Minimize context updates
3. Use lazy evaluation with function updaters
4. Leverage memoization for expensive computations
5. Split large machines into smaller actors
## Conclusion
Remember: XState excels at making complex logic predictable and maintainable. Always prioritize clarity and correctness over brevity.

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