# Testing Methodologies Standards for XState
This document outlines the recommended testing methodologies for XState state machines and actors. It covers strategies for unit, integration, and end-to-end testing, with a focus on best practices for the latest version of XState. Following these standards ensures maintainability, reliability, and correctness of XState-based applications.
## 1. General Testing Principles
### 1.1. Test Pyramid
* **Do This:** Adhere to the test pyramid, prioritizing unit tests, followed by integration tests, and then end-to-end tests.
* **Don't Do This:** Over-rely on end-to-end tests at the expense of unit and integration tests. This leads to brittle and slow test suites.
**Why?** A balanced test pyramid ensures faster feedback loops (unit tests), comprehensive coverage (integration tests), and realistic user flow validation (end-to-end tests).
### 1.2. Test-Driven Development (TDD)
* **Do This:** Consider using TDD to drive the design of your state machines. Write a test for a transition or guard before implementing it.
* **Don't Do This:** Implement complex state machine logic without first defining the expected behavior through tests.
**Why?** TDD encourages a clear understanding of requirements and results in more testable and modular state machine designs.
### 1.3. Test Coverage
* **Do This:** Aim for high test coverage of all states, transitions, guards, and actions within your state machines. Use code coverage tools to identify gaps in your tests.
* **Don't Do This:** Assume that achieving 100% coverage guarantees bug-free code. Focus on testing critical paths and edge cases.
**Why?** Test coverage is a metric that provides a measure of how much of the codebase is exercised by tests. High coverage reduces the risk of regressions and ensures a more stable application.
### 1.4. Test Isolation
* **Do This:** Isolate state machine tests by mocking external dependencies (services, API calls, etc.).
* **Don't Do This:** Allow external dependencies to directly influence the outcome of state machine tests. This can lead to flaky tests and makes debugging difficult.
**Why?** Isolation ensures that tests are deterministic and that failures are directly attributable to issues within the state machine.
### 1.5. Clear and Readable Tests
* **Do This:** Write tests that are easy to understand and maintain. Use descriptive test names and comments to explain the purpose of each test.
* **Don't Do This:** Write overly complex or cryptic tests that are difficult to understand or debug.
**Why?** Clear and readable tests enable easier collaboration and make it easier to identify the cause of failures.
## 2. Unit Testing
### 2.1. Focus
* **Do This:** Unit tests should focus on verifying the behavior of individual states, transitions, guards, and actions within a state machine.
* **Don't Do This:** Treat unit tests as integration tests by including interactions with external systems.
**Why?** Unit tests provide rapid feedback and isolate issues to specific parts of the state machine.
### 2.2. State Transition Testing
* **Do This:** Create tests that verify that the state machine transitions to the correct state in response to various events.
"""typescript
import { createMachine } from 'xstate';
import { createModel } from '@xstate/test';
const bookingMachine = createMachine({
id: 'booking',
initial: 'idle',
states: {
idle: {
on: {
START: {
target: 'pending',
actions: ['startBooking'],
},
},
},
pending: {
on: {
RESOLVE: {
target: 'success',
actions: ['resolveBooking'],
},
REJECT: {
target: 'failure',
actions: ['rejectBooking'],
},
},
},
success: {
type: 'final',
},
failure: {
on: {
RETRY: 'pending',
},
},
},
}, {
actions: {
startBooking: () => { console.log('Starting booking'); },
resolveBooking: () => { console.log('Resolving booking'); },
rejectBooking: () => { console.log('Rejecting booking'); },
}
});
const bookingModel = createModel(bookingMachine).withEvents({
START: {},
RESOLVE: {},
REJECT: {},
RETRY: {}
});
describe('Booking Machine', () => {
const testPlans = bookingModel.getShortestPathPlans();
testPlans.forEach((plan) => {
describe(plan.description, () => {
plan.paths.forEach((path) => {
it(path.description, async () => {
await path.test();
});
});
});
});
it('should have complete coverage', () => {
bookingModel.getAllStates().forEach((state) => {
expect(state.properties).not.toBeUndefined();
});
});
});
"""
* **Don't Do This:** Only test a subset of transitions, leaving edge cases and error scenarios untested.
**Why?** Thorough state transition testing ensures predictable and reliable behavior of the state machine. The "createModel" and "@xstate/test" tools are optimal for state machine testing.
### 2.3. Guard Condition Testing
* **Do This:** Write tests to verify that guard conditions correctly determine whether a transition can occur.
"""typescript
import { createMachine } from 'xstate';
import { createModel } from '@xstate/test';
const authMachine = createMachine({
id: 'auth',
initial: 'loggedOut',
context: {
isAdmin: false
},
states: {
loggedOut: {
on: {
LOGIN: {
target: 'loggedIn',
guard: 'isAdmin'
}
}
},
loggedIn: {
type: 'final'
}
}
}, {
guards: {
isAdmin: (context) => context.isAdmin
}
});
const authModel = createModel(authMachine).withEvents({
LOGIN: {}
});
describe('Auth Machine', () => {
it('should not transition to loggedIn if not admin', async () => {
const testMachine = authMachine.withContext({ isAdmin: false });
const snapshot = testMachine.transition('loggedOut', { type: 'LOGIN' });
expect(snapshot.changed).toBe(false);
expect(snapshot.value).toBe('loggedOut');
});
it('should transition to loggedIn if admin', async () => {
const testMachine = authMachine.withContext({ isAdmin: true });
const snapshot = testMachine.transition('loggedOut', { type: 'LOGIN' });
expect(snapshot.changed).toBe(true);
expect(snapshot.value).toBe('loggedIn');
});
});
"""
* **Don't Do This:** Neglect to test guard conditions, potentially allowing invalid transitions to occur.
**Why?** Guard conditions enforce business rules and constraints. Testing them ensures that these rules are correctly implemented.
### 2.4. Action Testing
* **Do This:** Verify that actions are executed correctly when transitions occur. Use mocks or spies to track action invocations.
"""typescript
import { createMachine, assign } from 'xstate';
import { createModel } from '@xstate/test';
import { vi, describe, it, expect } from 'vitest'
const sendEmail = vi.fn();
const emailMachine = createMachine({
id: 'email',
initial: 'idle',
context: {
emailAddress: ''
},
states: {
idle: {
on: {
SEND: {
target: 'sending',
actions: 'setEmailAddress'
}
}
},
sending: {
entry: 'sendEmail',
type: 'final'
}
}
}, {
actions: {
setEmailAddress: assign({
emailAddress: (context, event) => event.email
}),
sendEmail: sendEmail
}
});
describe('Email Machine', () => {
it('should execute the sendEmail action when transitioning to the sending state', () => {
const testMachine = emailMachine.withContext({ emailAddress: "test@example.com" });
testMachine.transition('idle', { type: 'SEND', email: 'test@example.com' });
expect(sendEmail).toHaveBeenCalledTimes(1);
});
});
"""
* **Don't Do This:** Skip testing actions, leaving the side effects of transitions unverified.
**Why?** Actions perform side effects, such as updating application state or interacting with external systems. Testing them ensures that these effects occur as expected.
### 2.5. Context Testing
* **Do This:** Ensure the context transitions as expected
* **Don't do This:** Skip context changes, especially computed contexts
"""typescript
import { createMachine, assign } from 'xstate';
import { createModel } from '@xstate/test';
import { vi, describe, it, expect } from 'vitest'
const userMachine = createMachine({
/** @xstate-layout N4IgpgJg5mIHgTgCwJYIDqgJwgOQLrmgNoAMAuwDUAlhANoB0AVAHQEsA7MAJwFUAZQDsdQAJgAtoaAA9EARgCsAdgAYjACgBMAFgBtAAwB2ALgDMAdl0ALAHYAjAGFjZq3oAYAHccOQ0gAHCn70AazkEcgB2IJS0DByCAO4AzAAsczZwAC4AKgAqA5QANH4A7kFQwHls4gBKMkSkrGz+2JzU9Iy4+ISk4pKSsnKSygLKyqr2iY7oQY0b2Wl4AdgBiAHIAUgBqU5Y6AEEs4uEAOiD0zZgYwZg4c05O2m53b7eUoK0lC1w+M6Q61e4S5pE4S06PqRQKZROAAXQ4wB0sBAchg4hA4YlEqlA1UqhUOhUOq3K43e40s22jU4QYyQ09t0kAAeXQ6AAUwG8M4w5gAUnk+gApIArYg4jicLgAnAATVgoNAgY5rIAKj2s0m1EokqYn5M77i4mUq2u60y00y9o9kL1Fp5W11h5SIA */
id: 'user',
initial: 'idle',
context: {
age: 0
},
states: {
idle: {
on: {
'USER.SET_AGE': {
actions: assign({
age: (context, event) => event.age
}),
target: 'idle',
description: 'Sets the age of the user'
}
}
}
}
})
describe('User Machine', () => {
it('should update the age', () => {
const testMachine = userMachine.withContext({ age: 20 });
const snapshot = testMachine.transition('idle', { type: 'USER.SET_AGE', age: 30 });
expect(snapshot.context.age).toBe(30);
});
});
"""
**Why:** The context of XState machines is a crucial element for managing state and data within the machine. Thoroughly testing context transitions ensures that data updates, state-dependent logic, and machine behavior are predictable and reliable. Neglecting context testing leads to unreliable applications and undermines the integrity of state management
## 3. Integration Testing
### 3.1. Focus
* **Do This:** Integration tests should focus on verifying the interactions between the state machine and its dependencies – user interfaces, external services, or other parts of the application.
* **Don't Do This:** Test the internal logic of the state machine in integration tests. This is the responsibility of unit tests.
**Why?** Integration tests ensure that the state machine is correctly integrated into the larger application.
### 3.2. UI Integration
* **Do This:** Use UI testing frameworks (e.g., Cypress, Jest with React Testing Library) to verify that the UI updates correctly in response to state changes in the state machine.
* **Don't Do This:** Manually verify UI updates, as this is time-consuming and error-prone.
**Why?** UI integration tests ensure that the user interface accurately reflects the state of the application.
### 3.3. Service Integration
* **Do This:** Mock external services to prevent integration tests from depending on the availability of these services. Use tools like "nock" or "msw" to mock HTTP requests.
* **Don't Do This:** Directly call external services in integration tests, as this can lead to flaky tests and obscure issues within the state machine.
**Why?** Service integration tests verify that the state machine correctly interacts with external systems.
### 3.4. Example
"""typescript
// Example using React Testing Library and mocked service
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';
import React from 'react';
import * as msw from 'msw';
import * as mswNode from 'msw/node';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
// Define a simple component that uses the state machine
const MyComponent = () => {
const paymentMachine = createMachine({
id: 'payment',
initial: 'idle',
context: {
paymentId: null,
error: null,
},
states: {
idle: {
on: {
INITIATE: 'pending',
},
},
pending: {
invoke: {
id: 'createPayment',
src: async () => {
const response = await fetch('/payment', { method: 'POST' });
if (!response.ok) {
throw new Error('Failed to create payment');
}
const data = await response.json();
return data;
},
onDone: {
target: 'success',
actions: assign({
paymentId: (_context, event) => event.data.id,
}),
},
onError: {
target: 'failure',
actions: assign({
error: (_context, event) => event.data.message,
}),
},
},
},
success: {
type: 'final',
},
failure: {
on: {
RETRY: 'pending',
},
},
},
});
const [state, send] = useMachine(paymentMachine);
return (
{state.matches('idle') && (
send({ type: 'INITIATE' })}>Initiate Payment
)}
{state.matches('pending') && <p>Creating payment...</p>}
{state.matches('success') && <p>Payment successful! Payment ID: {state.context.paymentId}</p>}
{state.matches('failure') && (
<>
<p>Payment failed: {state.context.error}</p>
send({ type: 'RETRY' })}>Retry
)}
);
};
const server = mswNode.setupServer(
msw.rest.post('/payment', (req, res, ctx) => {
return res(ctx.json({ id: '12345' }));
}),
);
beforeAll(() => server.listen());
afterAll(() => server.close());
msw.afterEach(() => server.resetHandlers());
describe('MyComponent', () => {
it('should initiate payment and display success message', async () => {
render();
userEvent.click(screen.getByText('Initiate Payment'));
await waitFor(() => screen.getByText('Payment successful! Payment ID: 12345'));
expect(screen.getByText('Payment successful! Payment ID: 12345')).toBeInTheDocument();
});
it('should handle payment failure and display error message', async () => {
server.use(
msw.rest.post('/payment', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: 'Payment failed' }));
}),
);
render();
userEvent.click(screen.getByText('Initiate Payment'));
await waitFor(() => screen.getByText('Payment failed: Payment failed'));
expect(screen.getByText('Payment failed: Payment failed')).toBeInTheDocument();
userEvent.click(screen.getByText('Retry'));
await waitFor(() => screen.getByText('Payment successful! Payment ID: 12345'));
expect(screen.getByText('Payment successful! Payment ID: 12345')).toBeInTheDocument();
});
});
"""
## 4. End-to-End (E2E) Testing
### 4.1. Focus
* **Do This:** E2E tests should focus on verifying critical user flows through the entire application, including the state machine, UI, and backend services.
* **Don't Do This:** Use E2E tests to cover every possible scenario. Focus on the most important user journeys.
**Why?** E2E tests ensure that the entire application works correctly from the user's perspective.
### 4.2. Tools
* **Do This:** Use E2E testing frameworks like Cypress, Playwright, or Puppeteer to automate user interactions and verify application behavior.
* **Don't Do This:** Rely on manual testing for critical user flows.
**Why?** Automated E2E tests provide consistent and reliable validation of application functionality.
### 4.3. Data Setup
* **Do This:** Set up test data before running E2E tests to ensure a consistent and predictable testing environment. Use database seeding or API calls to create the necessary data.
* **Don't Do This:** Assume that the application starts in a known state.
**Why?** Consistent test data prevents E2E tests from failing due to inconsistent application state.
### 4.4. Example with Cypress
"""javascript
// cypress/e2e/booking.cy.js
describe('Booking Flow', () => {
it('should book a flight successfully', () => {
cy.visit('/booking');
// Mock the API request using cy.intercept
cy.intercept('POST', '/api/flights', {
fixture: 'flights.json', // Load mock data from a fixture file
}).as('getFlights');
cy.get('[data-testid="departure-city"]').select('London');
cy.get('[data-testid="arrival-city"]').select('New York');
cy.get('[data-testid="date"]').type('2024-01-15');
cy.get('button[type="submit"]').click();
cy.wait('@getFlights'); // Wait for the mocked API call to complete
cy.get('[data-testid="flight-list-item"]').should('have.length.gt', 0);
cy.get('[data-testid="book-flight-button"]').first().click();
cy.get('[data-testid="confirmation-message"]').should('contain', 'Booking confirmed!');
});
});
// cypress/fixtures/flights.json
[
{
"id": "1",
"departureCity": "London",
"arrivalCity": "New York",
"date": "2024-01-15",
"price": 500
},
{
"id": "2",
"departureCity": "London",
"arrivalCity": "New York",
"date": "2024-01-15",
"price": 600
}
]
"""
### 4.5. Test Stability
* Do This: Add retry logic and timeouts when interacting with elements in the UI to account for potential delays or loading times.
* Don't do this: Rely on hardcoded delays (e.g., "setTimeout") as they can make tests slow and are not reliable.
**Why**: UI interactions might experience variability in timing, leading to transient test failures. Retry logic and timeouts make end-to-end (E2E) tests more robust by allowing the test to wait and retry interactions when elements are not immediately available or when asynchronous processes are in progress.
## 5. XState Test Model
### 5.1. Overview
* **Do This:** Utilize the "@xstate/test" library with "createModel" to automatically generate test plans from your state machine definitions.
* **Don't Do This:** Manually write individual test cases for every state and event combination.
**Why?** "createModel" automates test generation, ensuring comprehensive coverage and reducing the risk of missing critical scenarios.
### 5.2. Example
"""typescript
import { createMachine } from 'xstate';
import { createModel } from '@xstate/test';
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const lightModel = createModel(lightMachine, {
events: {
TIMER: () => ({})
}
});
const testPlans = lightModel.getShortestPathPlans();
testPlans.forEach((plan) => {
describe(plan.description, () => {
plan.paths.forEach((path) => {
it(path.description, async () => {
await path.test();
});
});
});
});
it('should have complete coverage', () => {
lightModel.getAllStates().forEach((state) => {
expect(state.properties).not.toBeUndefined();
});
});
"""
### 5.3. Custom Assertions
* Do This: Customize assertions to add specific checks that ensure both transition and internal state changes match expectations.
* Don't Do This: Omit assertions, relying solely on state transitions, which can lead to overlooking subtle errors within the machine's logic.
**Why**: Thoroughly testing internal changes and final states enhances the robustness of state machine tests, ensuring the machine behaves correctly under various conditions and that its outputs meet expected criteria. Explicit assertions help catch unexpected behavior and state corruption early, leading to more reliable and maintainable systems.
## 6. Visual Inspection and Debugging
### 6.1. State Visualizer
* **Do This:** Use the XState Visualizer (Stately Studio) to visually inspect the state machine and understand its behavior.
* **Don't Do This:** Rely solely on code inspection, as this can be difficult for complex state machines.
**Why?** The visualizer provides a clear and intuitive representation of the state machine, making it easier to identify potential issues.
### 6.2. Debugging
* **Do This:** Use debugging tools (e.g., "console.log", browser debugger) to trace the execution of state machine actions and transitions.
* **Don't Do This:** Rely solely on error messages, as they may not provide sufficient information to diagnose the root cause of issues.
**Why?** Debugging tools provide detailed insights into the behavior of the state machine, making it easier to identify and fix issues. Also use event listeners in inspector.
## 7. Performance Testing Considerations
### 7.1. Bottlenecks
* **Do This:** Identify potential performance bottlenecks within state machine actions, especially those that involve complex computations or I/O operations.
* **Don't Do This:** Assume that the state machine itself is always the cause of performance issues.
**Why?** Performance bottlenecks can degrade the responsiveness and scalability of the application.
### 7.2. Load Testing
* **Do This:** Perform load testing to assess the performance of the state machine under heavy load. Use tools like "k6" or "artillery" to simulate concurrent users and events.
* **Don't Do This:** Neglect to test the performance of the state machine under realistic load conditions.
**Why?** Load testing identifies performance limitations and helps ensure that the application can handle expected traffic.
### 7.3. Optimization
* **Do This:** Optimize performance bottlenecks by caching results, using more efficient algorithms, or offloading work to background tasks.
* **Don't Do This:** Prematurely optimize the state machine without first identifying and measuring performance bottlenecks.
**Why?** Performance optimization improves the responsiveness and scalability of the application.
## 8. Security Considerations
### 8.1. Input Validation
* **Do This:** Validate all inputs to the state machine, including events and context data, to prevent malicious or invalid data from entering the system and causing unexpected behavior.
* **Don't Do This:** Trust that inputs are always valid. Always validate data before using it.
**Why?** Input validation protects against security vulnerabilities such as injection attacks and denial-of-service attacks.
### 8.2. Access Control
* **Do This:** Enforce access control policies within the state machine to restrict access to sensitive states, transitions, or actions based on user roles or permissions.
* **Don't Do This:** Allow unauthorized users to trigger sensitive transitions or access sensitive data.
**Why?** Access control protects against unauthorized access and data breaches.
### 8.3. Error Handling
* **Do This:** Implement robust error handling within the state machine to gracefully handle unexpected errors or exceptions and prevent them from crashing the application or exposing sensitive information.
* **Don't Do This:** Silently ignore errors or allow them to bubble up to the user interface.
**Why?** Proper error handling improves the reliability and security of the application.
By adhering to these testing methodologies, development teams can ensure the quality, reliability, and security of XState-based applications. This comprehensive approach encompasses unit, integration, and end-to-end testing, leveraging the XState ecosystem's tools and libraries for optimal results.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for XState This document outlines the core architectural standards for developing XState applications. It focuses on fundamental patterns, project structure, and organization principles specifically applied to XState projects. These standards aim to improve maintainability, performance, and security while leveraging the latest XState features. ## 1. Overall Architecture and Project Structure ### 1.1. Standard: Component-Based Architecture **Do This:** Organize your application using a component-based architecture where each component encapsulates its own state machine. **Don't Do This:** Create monolithic state machines that manage the entire application state. Avoid tightly coupling components. **Why:** Component-based architectures promote modularity, reusability, and testability. Large, monolithic state machines become difficult to understand and maintain as the application grows. **Example:** """typescript // counterMachine.ts import { createMachine } from 'xstate'; export const counterMachine = createMachine({ id: 'counter', initial: 'idle', context: { count: 0 }, states: { idle: { on: { INC: { actions: ['increment'] }, DEC: { actions: ['decrement'] } } } }, actions: { increment: (context) => { context.count += 1; }, decrement: (context) => { context.count -= 1; } } }); // CounterComponent.tsx (React example) import React from 'react'; import { useMachine } from '@xstate/react'; import { counterMachine } from './counterMachine'; const CounterComponent = () => { const [state, send] = useMachine(counterMachine); return ( <div> <p>Count: {state.context.count}</p> <button onClick={() => send('INC')}>Increment</button> <button onClick={() => send('DEC')}>Decrement</button> </div> ); }; export default CounterComponent; """ ### 1.2. Standard: Feature-Based Folder Structure **Do This:** Organize your project directory by features or modules rather than by file type. **Don't Do This:** Having folders like "/components", "/services", "/utils" at the top level. **Why:** Feature-based organization improves discoverability and reduces the cognitive load on developers. It makes it easier to locate all the related files for a specific feature. **Example:** """ /src /counterFeature // Feature directory counterMachine.ts // XState Machine CounterComponent.tsx // React Component counter.styles.ts // Styles specific to the feature counter.test.ts // Tests /authFeature authMachine.ts AuthComponent.tsx authService.ts /app // Application wide files App.tsx index.tsx """ ### 1.3. Standard: Machine Definition Location **Do This:** Place machine definitions in dedicated files that describe their specific function within the feature. **Don't Do This:** Define machines directly within component files. The machine definition should be its own module. **Why:** Reduces complexity, makes the machine definition reusable, and promotes separation of concerns. **Example:** Refers to code example to the counterMachine definition in previous section. ### 1.4 Standard: State Chart Visualization **Do This:** Use Stately Studio or similar tools to visually design and document your state machines. **Don't Do This:** Rely solely on code to understand the structure and behavior of your machines. **Why:** Visualizations improve understanding, facilitate collaboration, and help identify potential issues early in the development process. Stately Studio also provides code generation capabilities which can improve developer efficiency and accuracy. ## 2. State Machine Design Principles ### 2.1. Standard: Explicit State Definitions **Do This:** Define all possible states of your machine explicitly, including "idle", "loading", "success", and "error" states. **Don't Do This:** Rely on implicit state management or boolean flags to represent state. **Why:** Explicit state definitions increase clarity, prevent unexpected behavior, and make it easier to reason about the state of your application. **Example:** """typescript import { createMachine } from 'xstate'; export const dataFetchingMachine = createMachine({ id: 'dataFetching', initial: 'idle', context: { data: null, error: null }, states: { idle: { on: { FETCH: 'loading' } }, loading: { entry : ['fetchData'], on: { RESOLVE: { target: 'success', actions: ['setData'] }, REJECT: { target: 'failure', actions: ['setError'] } } }, success: { type: 'final', entry: ['logData'] }, failure: { on: { RETRY: 'loading' }, entry: ['logError'] } }, actions: { setData: (context, event) => { context.data = event.data; }, setError: (context, event) => { context.error = event.error; }, logData: (context) => { console.log("Success: ", context.data); }, logError: (context) => { console.error("Error: ", context.error); }, fetchData: async (context, event) => { try { const data = await fetchData(); //Async function to fetch data return send({type: 'RESOLVE', data}) } catch (error) { return send({type: 'REJECT', error}) } } } }); async function fetchData() { // Simulate an API call return new Promise((resolve, reject) => { setTimeout(() => { const success = Math.random() > 0.2; // Simulate occasional failure if (success) { resolve({ message: "Data loaded successfully!" }); } else { reject(new Error("Failed to fetch data.")); } }, 1000); }); } """ ### 2.2. Standard: Guard Conditions for Transitions **Do This:** Use guard conditions ("guards") to control transitions based on context or event data. **Don't Do This:** Use imperative logic within actions to determine the next state. **Why:** Guards make the transition logic declarative and easier to understand. They separate decision-making logic from state transitions. **Example:** """typescript import { createMachine } from 'xstate'; interface Context { age: number; } type Event = { type: 'CHECK_AGE'; age: number }; export const ageVerificationMachine = createMachine<Context, Event>({ id: 'ageVerification', initial: 'unknown', context: { age: 0 }, states: { unknown: { on: { CHECK_AGE: [ { target: 'adult', guard: 'isAdult' }, { target: 'minor' } ] } }, adult: { type: 'final' }, minor: { type: 'final' } }, guards: { isAdult: (context, event) => { return event.age >= 18; } } }); """ ### 2.3. Standard: Context Management **Do This:** Define and manage the machine's context explicitly. Use actions to update the context based on events. **Don't Do This:** Modify external variables directly from within the machine. **Why:** Explicit context management ensures that all relevant data is encapsulated within the machine, making it easier to reason about state changes and data flow. **Example:** (Refer to counterMachine.ts example for an explicit context example.) ### 2.4 Standard: Statelessness of Machines **Do This:** Ensure that the machine definition itself is stateless. Store all dynamic data within the "context". **Don't Do This:** Bake-in dynamic data inside the machine definition; this defies its statelessness. **Why:** This allows multiple instances of the same machine to exist without sharing the state, maintaining integrity and predictability. ## 3. Action Implementation ### 3.1. Standard: Named Actions **Do This:** Use named actions that clearly describe the purpose of the action. **Don't Do This:** Use anonymous, inline actions, especially for complex logic. **Why:** Named actions improve readability and allow for reuse of action logic. **Example:** """typescript import { createMachine } from 'xstate'; export const lightMachine = createMachine({ id: 'light', initial: 'green', states: { green: { on: { TIMER: 'yellow' }, entry: ['logGreen'] }, yellow: { on: { TIMER: 'red' }, entry: ['logYellow'] }, red: { on: { TIMER: 'green' }, entry: ['logRed'] } }, actions: { logGreen: () => { console.log('Entering green state'); }, logYellow: () => { console.log('Entering yellow state'); }, logRed: () => { console.log('Entering red state'); } } }); """ ### 3.2. Standard: Side Effect Management **Do This:** Isolate side effects (e.g., API calls, DOM manipulations) within actions. Use "invoke" for asynchronous operations. **Don't Do This:** Perform side effects directly within components or services outside the machine. **Why:** Isolating side effects improves testability and makes it easier to manage asynchronous operations and potential errors. **Example** (Using "invoke" for data fetching - builds on the previous "dataFetchingMachine" example from 2.1) """typescript import { createMachine, send } from 'xstate'; export const dataFetchingMachine = createMachine({ id: 'dataFetching', initial: 'idle', context: { data: null, error: null }, states: { idle: { on: { FETCH: 'loading' } }, loading: { invoke: { id: 'fetchData', src: 'fetchDataService', onDone: { target: 'success', actions: ['setData'] }, onError: { target: 'failure', actions: ['setError'] } } }, success: { type: 'final', entry: ['logData'] }, failure: { on: { RETRY: 'loading' }, entry: ['logError'] } }, actions: { setData: (context, event) => { context.data = event.data; }, setError: (context, event) => { context.error = event.data; //Event.data contains the error object automatically for invokes }, logData: (context) => { console.log("Success: ", context.data); }, logError: (context) => { console.error("Error: ", context.error); } } }, { services: { fetchDataService: async (context, event) => { const data = await fetchData(); //Async function to fetch data return data; } } }); async function fetchData() { // Simulate an API call return new Promise((resolve, reject) => { setTimeout(() => { const success = Math.random() > 0.2; // Simulate occasional failure if (success) { resolve({ message: "Data loaded successfully!" }); } else { reject(new Error("Failed to fetch data.")); } }, 1000); }); } """ ### 3.3 Standard: Using "assign" Action **Do This:** Use the "assign" action provided by XState to update the machine's context immutably for clarity and predictability. Use the "raise" action for forwarding events to the same machine. **Don't Do This:** Mutate the context directly, or attempt to send events using side effects. **Why**: "assign" ensures predictable state transitions, which makes debugging and testing much easier. The "raise" action ensures events are properly processed within the machine. **Example:** """typescript import { createMachine, assign, raise } from 'xstate'; interface MyContext { value: number; } type MyEvent = { type: 'INC' } | { type: 'DEC' } | { type: 'NOTIFY' }; const myMachine = createMachine<MyContext, MyEvent>({ id: 'myMachine', initial: 'active', context: { value: 0 }, states: { active: { on: { INC: { actions: assign({ value: (context) => context.value + 1 }) }, DEC: { actions: assign({ value: (context) => context.value - 1 }) }, NOTIFY: { actions: raise({ type: 'INC' }) // Internally increment the value } } } } }); """ ### 3.4. Standard: Event Naming Conventions **Do This:** Use clear and consistent naming conventions for events. Use verbs or nouns that describe the event’s intent (e.g., "FETCH", "SUBMIT", "DATA_LOADED"). **Don't Do This:** Use generic or ambiguous event names (e.g., "EVENT", "ACTION"). **Why:** Clear event names improve readability and reduce the risk of confusion in complex state machines. ## 4. Integration with Frameworks ### 4.1. Standard: Hooks for State Management **Do This:** Use XState's provided hooks (e.g., "useMachine" in React) to connect machines to your UI components. **Don't Do This:** Manually subscribe to the machine's state updates or manage event sending imperatively outside component. **Why:** The XState hooks simplify integration with UI frameworks and handle lifecycle management automatically. **Example:** (React example demonstrating the use of useMachine, builds on counterMachine.ts example from 1.1.) """typescript import React from 'react'; import { useMachine } from '@xstate/react'; import { counterMachine } from './counterMachine'; const CounterComponent = () => { const [state, send] = useMachine(counterMachine); return ( <div> <p>Count: {state.context.count}</p> <button onClick={() => send('INC')}>Increment</button> <button onClick={() => send('DEC')}>Decrement</button> </div> ); }; export default CounterComponent; """ ### 4.2. Standard: Derived State **Do This**: Consider creating derived state within your components using selectors based on the machine's context. **Don't Do This**: Duplicate state or create complex logic within your components that should be handled by the state machine **Why**: This keeps the state machine as the single source of truth and prevents inconsistencies. **Example:** """typescript //Counter Component import React from 'react'; import { useMachine } from '@xstate/react'; import { counterMachine } from './counterMachine'; const CounterComponent = () => { const [state, send] = useMachine(counterMachine); const isEven = state.context.count % 2 === 0; //Derived value return ( <div> <p>Count: {state.context.count} (Even: {isEven ? 'Yes' : 'No'})</p> <button onClick={() => send('INC')}>Increment</button> <button onClick={() => send('DEC')}>Decrement</button> </div> ); }; export default CounterComponent; """ ## 5. Testing and Debugging ### 5.1. Standard: Unit Tests for State Machines **Do This:** Write unit tests for your state machines to ensure that they transition correctly and handle events as expected. Use "@xstate/test" for thorough testing. **Don't Do This:** Rely solely on manual testing or integration tests. **Why:** Unit tests provide fast feedback and help prevent regressions as the application evolves. **Example** (Using @xstate/test): """typescript import { createMachine } from 'xstate'; import { createModel } from '@xstate/test'; const bookingMachine = createMachine({ id: 'booking', initial: 'idle', context: { seats: 0 }, states: { idle: { on: { INITIATE: 'pending' } }, pending: { on: { RESOLVE: {target: 'confirmed', actions: assign({seats : (context, event) => event.seats})}, REJECT: 'rejected' } }, confirmed: { type: 'final' }, rejected: { on: { RETRY: 'pending' } } } }); const bookingModel = createModel(bookingMachine).withEvents({ INITIATE: {}, RESOLVE: { exec: async () => { return new Promise((resolve) => { setTimeout(() => { resolve(true); }, 500); }); }, cases: [{ seats: 5 }, { seats: 10 }] }, REJECT: {}, RETRY: {} }); describe('booking machine', () => { const testPlans = bookingModel.getShortestPathPlans(); testPlans.forEach((plan) => { describe(plan.description, () => { plan.paths.forEach((path) => { it(path.description, async () => { await path.test(); }); }); }); }); it('should have full coverage', () => { return bookingModel.testCoverage(); }); }); """ ### 5.2. Standard: Debugging Tools **Do This:** Use the XState Visualizer in Stately Studio and browser developer tools to inspect the machine's state and events during runtime. **Don't Do This:** Rely solely on "console.log" statements for debugging. **Why:** Visual debugging tools provide a more intuitive and efficient way to understand the behavior of your state machines. XState inspector tools allow for connecting your running XState machines to the Stately Studio visualizer seamlessly. ### 5.3. Standard: Mock Services in Tests **Do This:** Mock external services (e.g., API calls) in your tests to isolate the state machine and ensure deterministic behavior. **Don't Do This:** Make real API calls during unit tests. **Why:** Mocking services makes tests faster, more reliable, and independent of external dependencies. ## 6. Advanced Architectural Patterns ### 6.1. Standard: Hierarchical and Parallel States **Do This:** Use hierarchical (nested) and parallel states to model complex state transitions and concurrent activities. **Don't Do This:** Flatten complex state logic into a single level of states due to added complexity. **Why:** Hierarchical and parallel states improve the organization and readability of complex state machines. **Example (Hierarchical/Nested States):** """typescript import { createMachine } from 'xstate'; export const audioPlayerMachine = createMachine({ id: 'audioPlayer', initial: 'idle', states: { idle: { on: { PLAY: 'playing' } }, playing: { states: { buffering: { on: { LOADED: 'ready' } }, ready: { on: { SEEK: 'buffering' } } }, initial: 'buffering', on: { PAUSE: 'paused', STOP: 'idle' } }, paused: { on: { PLAY: 'playing', STOP: 'idle' } } } }); """ ### 6.2. Standard: History States **Do This:** Employ history states when you need to return to a previously active substate within a hierarchical state. **Don't Do This:** Manually store the last active state. **Why:** History states simplify returning to a previous state after an interruption and reduce the complexity of manual state tracking. """typescript import { createMachine } from 'xstate'; const documentMachine = createMachine({ id: 'document', initial: 'editing', states: { editing: { states: { text: { on: { FORMAT: 'formatting' } }, formatting: { type: 'history' } }, initial: 'text', on: { SAVE: 'saving' } }, saving: { type: 'final' } } }); """ ### 6.3. Standard: Compound Actions **Do This:** Utilize compound actions (actions that call other actions or send events) to compose complex behaviors. **Don't Do This:** Create excessively long action definitions. **Why:** It improves reusability and readability. XState's "choose" action can act as a "conditional action". **Example:** """typescript import { createMachine, assign, choose } from 'xstate'; interface MyContext { attempts: number; success: boolean; } type MyEvent = { type: 'SUBMIT' } | { type: 'RETRY' } | { type: 'SUCCESS' } | { type: 'FAILURE' }; const submissionMachine = createMachine<MyContext, MyEvent>({ id: 'submission', initial: 'idle', context: { attempts: 0, success: false }, states: { idle: { on: { SUBMIT: 'submitting' } }, submitting: { entry: 'incrementAttempts', invoke: { id: 'submitData', src: 'submitDataService', onDone: { target: 'success', actions: 'markSuccess' }, onError: { target: 'failure' } } }, success: { type: 'final' }, failure: { on: { RETRY: { target: 'submitting', cond: 'canRetry' } }, exit: choose([ { cond: (context) => context.attempts > 3, actions: () => console.log("Max attempts reached") } ]) } }, actions: { incrementAttempts: assign({ attempts: (context) => context.attempts + 1 }), markSuccess: assign({ success: true }) }, guards: { canRetry: (context) => context.attempts < 3 }, services: { submitDataService: async () => { // Simulate an API call return new Promise((resolve, reject) => { setTimeout(() => { const success = Math.random() > 0.5; if (success) { resolve("Data submitted successfully!"); } else { reject(new Error("Submission failed.")); } }, 1000); }); } } }); """ This coding standard provides a strong foundation for building robust and maintainable XState applications. By adhering to these guidelines, development teams can ensure consistency, improve code quality, and leverage the full power of the XState library.
# API Integration Standards for XState This document outlines coding standards for integrating XState state machines with backend services and external APIs. It is intended to provide a consistent and maintainable approach across all XState API integrations. ## 1. General Principles ### 1.1. Separation of Concerns **Do This:** Isolate API logic from state machine logic as much as possible. **Don't Do This:** Directly embed API calls within state actions or guards without abstraction. **Why:** Separating concerns makes the state machine more readable, testable, and maintainable. Changes to the API implementation won't necessitate changes to the state machine's core logic. **Example:** """typescript // Anti-pattern: Tightly coupled API call import { createMachine, assign } from 'xstate'; const userMachine = createMachine({ id: 'user', initial: 'idle', context: { userData: null, error: null, }, states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { invoke: { id: 'fetchUser', src: (context) => { return fetch('/api/user') .then((response) => response.json()) .catch((error) => { console.error("API Error:", error); throw error; // Re-throw to reject the promise }); }, onDone: { target: 'success', actions: assign({ userData: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final', }, failure: { on: { RETRY: 'loading', }, }, }, }); // Better pattern: Decoupled API call using a service import { createMachine, assign } from 'xstate'; // API service abstraction const fetchUserService = (context) => { return fetch('/api/user') .then((response) => response.json()) .catch((error) => { console.error("API Error:", error); throw error; // This is crucial for onError to work correctly. }); }; const userMachine = createMachine({ id: 'user', initial: 'idle', context: { userData: null, error: null, }, states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { invoke: { id: 'fetchUser', src: fetchUserService, onDone: { target: 'success', actions: assign({ userData: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final', }, failure: { on: { RETRY: 'loading', }, }, }, }); """ ### 1.2. Declarative over Imperative **Do This:** Define API integration declaratively using "invoke" and "actions". **Don't Do This:** Mutate state directly within API callbacks or side effects outside of XState’s control. **Why:** Declarative approaches lead to more predictable and testable state management because XState controls all state mutations. Imperative approaches can introduce race conditions, unexpected state transitions, and make testing cumbersome, violating the fundamental principles. **Example:** """typescript // Anti-pattern: Imperative state mutation outside of state machine context let externalUserData = null; const userMachine = createMachine({ id: 'user', initial: 'idle', states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { entry: () => { fetch('/api/user') .then((response) => response.json()) .then((data) => { externalUserData = data; // Direct mutation of external variable - BAD }); }, after: { 3000: 'success', }, }, success: { type: 'final', }, }, }); // Correct pattern: Declarative state updates via assign action import { createMachine, assign } from 'xstate'; const userMachine = createMachine({ id: 'user', initial: 'idle', context: { userData: null, }, states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { invoke: { id: 'fetchUser', src: () => fetch('/api/user').then((response) => response.json()), onDone: { target: 'success', actions: assign({ userData: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final' }, failure: { on: { RETRY: 'loading', }, }, }, }); """ ### 1.3. Context Management **Do This:** Store API-related data (e.g., request parameters, response data, error messages) in the state machine's context. **Don't Do This:** Use global variables or component-level state outside of the context. **Why:** Centralized context simplifies debugging, testing, and state hydration/rehydration. It adheres to the single source of truth principle. **Example:** """typescript // Anti-pattern: Using an external variable as flag let isSaving = false; const saveMachine = createMachine({ id: 'save', initial: 'idle', states: { idle: { on: { SAVE: { target: 'saving', actions: () => { isSaving = true; // external mutation setTimeout(() => { isSaving = false; //external mutation; }, 2000); } } } }, saving: { after: { 2000: "saved" } }, saved: { type: 'final' } } }) //DO This: import { createMachine, assign } from 'xstate'; const saveMachine = createMachine({ id: 'save', initial: 'idle', context: { isSaving: false }, states: { idle: { on: { SAVE: { target: 'saving', actions: assign({ isSaving: true }) } } }, saving: { after: { 2000: "saved" }, exit: assign({ isSaving: false }) }, saved: { type: 'final' } } }) """ ## 2. API Service Invocation ### 2.1. Using "invoke" **Do This:** Use the "invoke" property to define API service invocations. Configure "onDone" and "onError" for handling successful responses and errors. Provide unique "id" for each Invoke configuration. **Don't Do This:** Call APIs directly within action functions or guards without using "invoke", as this bypasses XState's lifecycle management features. **Why:** "invoke" manages the lifecycle of the promise, including cancellation, loading states, and error handling. **Example:** """typescript import { createMachine, assign } from 'xstate'; const todoMachine = createMachine({ id: 'todo', initial: 'idle', context: { todos: [], error: null, }, states: { idle: { on: { LOAD: { target: 'loading', }, }, }, loading: { invoke: { id: 'loadTodos', src: () => fetch('/api/todos').then((response) => response.json()), onDone: { target: 'loaded', actions: assign({ todos: (context, event) => event.data }), }, onError: { target: 'failed', actions: assign({ error: (context, event) => event.data }), }, }, }, loaded: { type: 'final', }, failed: { on: { RETRY: 'loading', }, }, }, }); """ ### 2.2. Error Handling **Do This:** Implement robust error handling for all API requests. Use "onError" in "invoke" to transition to an error state and store the error in the context. Consider global error handling mechanisms for unrecoverable errors. Ensure that "src" throws the error. **Don't Do This:** Ignore potential API errors or let them crash the application. **Why:** Proper error handling improves the user experience, allows for graceful recovery, and aids in debugging. **Example:** """typescript import { createMachine, assign } from 'xstate'; const userMachine = createMachine({ id: 'user', initial: 'idle', context: { userData: null, error: null, }, states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { invoke: { id: 'fetchUser', src: () => fetch('/api/user') .then((response) => { if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return response.json(); }) .catch((error) => { console.error("API Error:", error); // Log the error throw error; // Re-throw the error to trigger onError }), onDone: { target: 'success', actions: assign({ userData: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final', }, failure: { on: { RETRY: 'loading', }, }, }, }); """ ### 2.3. Cancellation **Do This:** Utilize the "invoke" feature. When transitioning out of a "loading" state that initiated an API request, XState automatically cancels the invoked service (the promise). Ensure API requests support cancellation (e.g., using "AbortController"). **Don't Do This:** Leave long-running API requests running in the background when they are no longer needed. **Why:** Cancellation is a core feature, preventing race conditions and wasted resources. **Example:** """typescript import { createMachine } from 'xstate'; const longRunningTask = (context) => (send) => { const controller = new AbortController(); const signal = controller.signal; fetch('/api/long-task', { signal }) .then((response) => response.json()) .then((data) => { send({ type: 'RESOLVE', data }); }) .catch((error) => { if (error.name === 'AbortError') { console.log('Fetch aborted'); } else { send({ type: 'REJECT', error }); } }); return () => { controller.abort(); // Cancel the fetch request }; }; const machine = createMachine({ id: "cancelableTask", initial: "idle", states: { idle: { on: { START: "loading" } }, loading: { invoke: { id: "myTask", src: longRunningTask, onDone: { target: "success", actions: (context, event) => { console.log("Success", event.data); } }, onError: { target: "failure", actions: (context, event) => { console.log("Failure", event.data); } } }, on: { CANCEL: "idle" // This will cancel the API fetch! } }, success: { type: 'final', }, failure: { type: 'final', } } }); """ ### 2.4. Debouncing and Throttling API Calls **Do This:** Implement debouncing or throttling for API calls that are triggered frequently (e.g., on input change). Use libraries such as "lodash" or "rxjs" to achieve this. **Don't Do This:** Allow API calls to be triggered excessively, leading to performance issues or API rate limits. **Why:** Debouncing and throttling optimize API usage and improve application responsiveness. **Example:** """typescript import { createMachine, assign } from 'xstate'; import { debounce } from 'lodash'; const searchMachine = createMachine({ id: 'search', initial: 'idle', context: { searchTerm: '', searchResults: [], error: null, }, states: { idle: { on: { INPUT: { target: 'debouncing', actions: assign({ searchTerm: (context, event) => event.value }), }, }, }, debouncing: { entry: assign((context) => { // Debounce the API call debouncedSearch(context.searchTerm); return context; }), after: { 300: 'loading', }, on: { INPUT: { target: 'debouncing', actions: assign({ searchTerm: (context, event) => event.value }), }, }, }, loading: { invoke: { id: 'searchAPI', src: (context) => fetch("/api/search?q=${context.searchTerm}").then((response) => response.json() ), onDone: { target: 'success', actions: assign({ searchResults: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final', }, failure: { on: { INPUT: { target: 'debouncing', actions: assign({ searchTerm: (context, event) => event.value }), }, }, }, }, }); const debouncedSearch = debounce((searchTerm) => { // Send an event *into* the machine so the transition to "loading" // will occur. Find the service and use "send". // Requires a "useService" hook. }, 300); """ ## 3. Data Transformation and Validation ### 3.1. Request Transformation **Do This:** Transform data before sending it to the API. Perform this transformation in actions triggered before the "invoke". **Don't Do This:** Directly pass UI form data to the API without validation or transformation. **Why:** Cleansing and shaping the outgoing data ensures data integrity and conforms to the API's expected format. **Example:** """typescript import { createMachine, assign } from 'xstate'; const submitFormMachine = createMachine({ id: 'submitForm', initial: 'idle', context: { formData: {}, apiData: {}, submissionResult: null, error: null, }, states: { idle: { on: { UPDATE_FORM: { actions: assign({ formData: (context, event) => ({ ...context.formData, [event.name]: event.value }) }), }, SUBMIT: { target: 'transforming', actions: assign({ apiData: (context) => { const { name, email, age } = context.formData; // Transform form data to API format return { full_name: name, email_address: email, age: parseInt(age, 10), }; }, }), }, }, }, transforming: { entry: (context) => { console.log("Transformed data:", context.apiData); }, after: { 100: 'submitting', }, }, submitting: { invoke: { id: 'submitAPI', src: (context) => fetch('/api/submit', { method: 'POST', body: JSON.stringify(context.apiData), }).then((response) => response.json()), onDone: { target: 'success', actions: assign({ submissionResult: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final', }, failure: { on: { RETRY: 'submitting', }, }, }, }); """ ### 3.2. Response Transformation **Do This:** Transform API data before storing it in the context. **Don't Do This:** Directly use API responses in the UI without transformation. **Why:** Response transformations allow you to decouple your UI from API changes. **Example:** Similar to Request Transformation - you apply transformation to "event.data" in the "assign" action in "onDone". ### 3.3. Validation **Do This:** Validate both request and response data. Libraries like "yup" or "zod" can be used to validate data schemas. Implement custom validation logic where necessary. **Don't Do This:** Assume that API data is always correct or safe. **Why:** Validation protects the data and guarantees that what is being sent to the API or rendered on the UI meets specific constraints. **Example:** """typescript import { createMachine, assign } from 'xstate'; import * as yup from 'yup'; const userSchema = yup.object().shape({ id: yup.number().required(), name: yup.string().required(), email: yup.string().email(), }); const userMachine = createMachine({ id: 'user', initial: 'idle', context: { userData: null, error: null, }, states: { idle: { on: { FETCH: { target: 'loading', }, }, }, loading: { invoke: { id: 'fetchUser', src: () => fetch('/api/user').then((response) => response.json()), onDone: { target: 'success', actions: assign({ userData: (context, event) => { try { return userSchema.validateSync(event.data); } catch (error) { console.error("Validation Error:", error); throw error; // Re-throw validation error to "onError" } }, }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: { type: 'final' }, failure: { on: { RETRY: 'loading', }, }, }, }); """ ## 4. Authentication and Authorization ### 4.1. Token Management **Do This:** Use secure storage mechanisms (e.g., "localStorage" with caution, or cookies with "httpOnly" flag) to store authentication tokens. Context can be used to store if the user is correctly authenticated. **Don't Do This:** Store sensitive tokens in global variables or expose them in client-side code directly. **Why:** Secure token management protects user credentials from unauthorized access. ### 4.2. Interceptors **Do This:** Implement request interceptors to automatically add authentication headers (e.g., Bearer tokens) to API requests. Libraries like "axios" and "ky" provide interceptor mechanisms. **Don't Do This:** Manually add authentication headers to every API request. **Why:** Interceptors centralize authentication logic and prevent code duplication. ### 4.3. Conditional Transitions **Do This:** Implement conditional transitions based on authentication state. For example, redirect unauthenticated users to a login page. Use "guards" in transitions. **Don't Do This:** Allow unauthenticated users to access protected resources. **Why:** Conditional transitions enforce authorization rules and protect sensitive data. **Example:** """typescript import { createMachine, assign } from 'xstate'; const authMachine = createMachine({ id: 'auth', initial: 'checking', context: { isAuthenticated: false, token: null, }, states: { checking: { entry: assign((context) => { // Check for token in local storage const token = localStorage.getItem('access_token'); return { ...context, token }; }), always: [ { target: 'authenticated', cond: (context) => !!context.token }, { target: 'unauthenticated' }, ], }, authenticated: { type: 'final', }, unauthenticated: { on: { LOGIN: { target: 'loggingIn', }, }, }, loggingIn: { invoke: { id: 'loginUser', src: (context, event) => { // Simulate API call return new Promise((resolve, reject) => { setTimeout(() => { if (event.username === 'user' && event.password === 'password') { const token = 'fake_access_token'; localStorage.setItem('access_token', token); resolve({ token }); } else { reject(new Error('Invalid credentials')); } }, 1000); }); }, onDone: { target: 'authenticated', actions: assign((context, event) => ({ isAuthenticated: true, token: event.data.token, })), }, onError: { target: 'unauthenticated', actions: assign({ isAuthenticated: false, token: null, }), }, }, } }, }); """ ## 5. Testing ### 5.1. Unit Tests **Do This:** Write unit tests for all state machines, focusing on transitions, context updates, and API service invocations. Mocks function calls to external source. **Don't Do This:** Deploy state machines without thorough testing. **Why:** Unit tests ensure the correctness and robustness of your state machines. **Example:** """typescript import { interpret } from 'xstate'; import { userMachine } from './userMachine'; // Assuming the machine is in a separate file describe('userMachine', () => { it('should transition from idle to loading on FETCH event', () => { const service = interpret(userMachine); service.start(); service.send({ type: 'FETCH' }); expect(service.state.value).toBe('loading'); }); it('should assign user data on successful fetch', (done) => { const service = interpret(userMachine.withConfig({ services: { fetchUser: () => Promise.resolve({ id: 1, name: 'Test User' }), }, })); service.onTransition((state) => { if (state.value === 'success') { expect(state.context.userData).toEqual({ id: 1, name: 'Test User' }); done(); } }); service.start(); service.send({ type: 'FETCH' }); }); it('should handle errors during fetch', (done) => { const service = interpret(userMachine.withConfig({ services: { fetchUser: () => Promise.reject(new Error('Failed to fetch user')), }, })); service.onTransition((state) => { if (state.value === 'failure') { expect(state.context.error).toBeInstanceOf(Error); done(); } }); service.start(); service.send({ type: 'FETCH' }); }); }); """ ### 5.2. Integration Tests **Do This:** Write integration tests to verify the interaction between the state machine and actual APIs. **Don't Do This:** Rely solely on unit tests when API integration is involved. **Why:** Integration tests ensure that the state machine works correctly with real-world API endpoints. ### 5.3. Mocking **Do This:** Use mocking libraries (e.g., "jest.mock", "nock") to mock API responses during testing. This lets you simulate different scenarios, like success, failure, timeouts, and edge cases. **Don't Do This:** Test against live APIs in all test environments. **Why:** Mocking isolates the state machine from external dependencies, enabling consistent and reliable testing. ## 6. Code Organization ### 6.1. Modular Structure **Do This:** Break large state machines into smaller, reusable modules. Use hierarchical state machines to organize complex logic. **Don't Do This:** Create monolithic state machines that are difficult to understand and maintain. **Why:** Modular code is easier to read, test, and reuse. ### 6.2. Consistent Naming **Do This:** Follow consistent naming conventions for states, events, actions, and guards. **Don't Do This:** Use inconsistent or ambiguous names. **Why:** Consistent naming improves readability and maintainability. ### 6.3. Documentation **Do This:** Document all state machines, including their states, events, actions, guards, and API integrations. **Don't Do This:** Leave state machines undocumented. **Why:** Documentation helps other developers understand and maintain the code. ## 7. Conclusion By adhering to these coding standards, you can create robust, maintainable, and well-tested XState applications that effectively integrate with backend services and external APIs. Remember to adapt these standards to your specific project requirements and development environment.
# Deployment and DevOps Standards for XState This document outlines the standards for deploying and managing XState state machines in production environments. It covers build processes, CI/CD pipelines, production considerations, monitoring, and scaling relevant for XState applications. Following these standards will result in more reliable, performant, and maintainable XState-powered systems. ## 1. Build Processes and CI/CD ### 1.1. Automated Builds **Standard:** Automate the build process using a CI/CD pipeline. **Do This:** * Use a CI/CD tool like GitHub Actions, GitLab CI, CircleCI, or Jenkins. * Define a build pipeline that lints, tests, and packages your XState code. * Automatically trigger builds on code commits and pull requests. **Don't Do This:** * Manually build and deploy code. * Skip linting and testing in the build process. **Why:** Automated builds ensure consistent code quality and rapid feedback on code changes. **Example (GitHub Actions):** """yaml name: XState CI/CD on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 18 uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm install - name: Lint run: npm run lint # Assumes you have a linting script - name: Test run: npm run test # Assumes you have a testing script - name: Build run: npm run build # Assumes you have a build script - name: Deploy if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | # Your deployment script here (e.g., deploying to AWS S3, Netlify, etc.) echo "Deploying to production..." # replace with actual deploy steps using env vars env: #replace with actual env vars appropriate for your deployment target AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} """ ### 1.2. Versioning **Standard:** Employ semantic versioning for your XState machine and related packages. **Do This:** * Use semantic versioning (semver) to track changes (e.g., "1.2.3"). * Automate version bumping as part of your CI/CD pipeline using tools like "npm version" or "standard-version". **Don't Do This:** * Manually manage versions without a clear strategy. * Introduce breaking changes without bumping the major version. **Why:** Semantic versioning helps consumers of your XState machines understand the impact of updates. **Example:** """bash # Using npm version to bump the patch version npm version patch -m "chore(release): Bump version to %s" # Using standard-version to automate versioning based on commit messages npx standard-version """ ### 1.3. Artifact Management **Standard:** Store build artifacts (e.g., compiled JavaScript files, type definitions) in an artifact repository. **Do This:** * Use a package registry like npm, or a cloud storage service like AWS S3 or Google Cloud Storage. * Tag artifacts with the version number. **Don't Do This:** * Store artifacts directly in your Git repository. * Lack versioning for your build artifacts. **Why:** Artifact management simplifies deployment and rollback processes. **Example (Publishing to npm):** """json // package.json { "name": "@your-org/your-xstate-machine", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "publish": "npm publish --access public" }, "dependencies": { "xstate": "^5.3.0" }, "devDependencies": { "typescript": "^5.0.0" } } """ """bash npm run build npm publish --access public """ ## 2. Production Considerations ### 2.1. State Machine Hydration/Dehydration **Standard:** Implement strategies for persisting and restoring the XState machine's state in production. **Do This:** * Use "JSON.stringify" and "JSON.parse" together with ".withContext()" for simple state persistence. * Implement custom serialization/deserialization logic for more complex data types in the context. * Consider using external storage (e.g., databases, Redis) for large or sensitive state data. **Don't Do This:** * Store the entire XState machine instance in a database. Just persist the machine's context and current state. * Expose sensitive data in the serialized state. **Why:** Persistence ensures stateful applications survive restarts or failures. **Example:** """javascript import { createMachine } from 'xstate'; const machine = createMachine({ id: 'myMachine', initial: 'idle', context: { count: 0, user: { id: 1, name: 'John Doe' } }, states: { idle: { on: { INCREMENT: { actions: (context) => { context.count += 1; } } } } } }); // Persist the state function persistState(state) { localStorage.setItem('myMachineState', JSON.stringify(state)); } // Restore the state function restoreState() { const storedState = localStorage.getItem('myMachineState'); if (storedState) { return machine.resolveState(JSON.parse(storedState)); } return machine.initialState; } // Usage let currentState = restoreState(); console.log(currentState.context); // Update the state currentState = machine.transition(currentState, 'INCREMENT'); console.log(currentState.context); persistState(currentState); """ ### 2.2. Environment Variables and Configuration **Standard:** Externalize configuration using environment variables or configuration files. **Do This:** * Store sensitive information (e.g., API keys, database passwords) in environment variables. * Use a library such as "dotenv" in development or utilize platform-specific environment variable configurations (e.g., AWS Lambda environment variables) in production. * Use configuration files (e.g., JSON, YAML) to define environment-specific settings. **Don't Do This:** * Hardcode configuration values directly in your XState machine definitions. * Commit sensitive information to your repository. **Why:** Externalized configuration enables flexible deployments across different environments without modifying code. **Example:** """javascript // Accessing environment variables in an XState machine import { createMachine } from 'xstate'; const apiEndpoint = process.env.API_ENDPOINT || 'https://default.example.com'; // using a default value const machine = createMachine({ id: 'apiMachine', initial: 'idle', states: { idle: { on: { FETCH: { target: 'loading', actions: () => { console.log("Fetching from ${apiEndpoint}"); // Replace with actual API call using apiEndpoint } } } }, loading: {} } }); // Set API_ENDPOINT environment variable before running // API_ENDPOINT=https://production.example.com node your-script.js """ ### 2.3. Error Handling and Logging **Standard:** Implement robust error handling and logging mechanisms. **Do This:** * Use "try...catch" blocks within actions, services, and guards to catch exceptions. * Implement "onError" handlers to gracefully handle errors within the machine. * Log important events, transitions, errors, and warnings using a centralized logging system (e.g., Winston, Log4js). * Include context information in log messages for easier debugging. **Don't Do This:** * Ignore errors or allow them to crash the application silently. * Log sensitive information, such as passwords or API keys. * Over-log, creating too much noise in the logs. **Why:** Proper error handling and logging are crucial for identifying and resolving issues in production. **Example:** """javascript import { createMachine } from 'xstate'; import logger from './logger'; // Hypothetical logger module import { send } from 'xstate'; const machine = createMachine({ id: 'errorMachine', initial: 'idle', states: { idle: { on: { TRIGGER_ERROR: { target: 'loading', actions: () => { logger.info('Triggering intentional error'); } } } }, loading: { invoke: { id: 'failingService', src: () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('Intentional service failure')); }, 1000); }); }, onDone: { target: 'success' }, onError: { target: 'failure', actions: (context, event) => { logger.error('Service failed', event.data); // Log the error // Optionally, send a custom event in the failure state return send({ type: 'CUSTOM_ERROR', error: event.data }) } } } }, success: { type: 'final', entry: () => logger.info('Operation successful') }, failure: { entry: () => logger.error('Operation failed') } } }); """ ### 2.4. Monitoring and Alerting **Standard:** Implement monitoring and alerting to track the health and performance of your XState applications. **Do This:** * Monitor key metrics, such as state transition frequency, error rates, and service latency. * Use tools like Prometheus, Grafana, Datadog, or New Relic to collect and visualize metrics. * Set up alerts to notify you of critical issues, such as high error rates or stalled state machines. * Monitor machine context size to avoid memory issues. **Don't Do This:** * Ignore performance metrics or assume your XState machines are always working correctly. * Set up alerts for non-critical issues, leading to alert fatigue. **Why:** Monitoring and alerting enable you to proactively identify and resolve issues before they impact users. **Example:** (This example focuses on the *concept* as actual monitoring integration depends heavily on the specific monitoring tool used and the deployment environment) """javascript // Hypothetical example of monitoring state transitions import { createMachine } from 'xstate'; //import monitoringService from './monitoringService'; // Hypothetical monitoring service const machine = createMachine({ id: 'monitoringMachine', initial: 'idle', states: { idle: { on: { START: { target: 'running', actions: () => { //monitoringService.incrementCounter('state.idle.start'); console.log( 'incrementCounter(state.idle.start)' ); } } } }, running: { on: { STOP: "stopped", ERROR: "errored" }, entry: () => { console.log( 'incrementCounter(state.running.entry)' ); //monitoringService.incrementCounter('state.running.entry'); }, exit: () => { console.log( 'incrementCounter(state.running.exit)' ); //monitoringService.incrementCounter('state.running.exit'); } }, stopped: { type: 'final' }, errored: { type: 'final' } } }); // Example of using the machine let currentState = machine.initialState; currentState = machine.transition(currentState, 'START'); // Increment state.idle.start and state.running.entry // In a monitoring service (hypothetical): // function incrementCounter(metricName) { // // Logic to send metricName to a monitoring tool like Prometheus or Datadog // } """ ### 2.5. Feature Flags **Standard:** Implement feature flags to control the rollout of new features or changes to XState machines. **Do This:** * Use a feature flag management system (e.g., LaunchDarkly, Split.io, Unleash). * Wrap new features or changes to machine logic with feature flags. * Gradually roll out features to a subset of users before releasing them to everyone. * Ensure feature flags can be toggled independently of code deployments. **Don't Do This:** * Leave feature flags in the codebase indefinitely. * Create overly complex feature flag logic within the XState machine. **Why:** Feature flags enable safe and controlled releases, reducing the risk of impacting all users with a buggy or poorly performing feature. They also support A/B testing and experimentation. **Example:** """javascript import { createMachine } from 'xstate'; //import featureFlags from './featureFlags'; // Hypothetical feature flag module const machine = createMachine({ id: 'featureFlagMachine', initial: 'idle', states: { idle: { on: { START: { target: 'featureA', // or 'featureB' based on the flag //guard: () => featureFlags.isFeatureAEnabled() //guard is not needed because it targets based on the result target: () => { // Use a function to dynamically determine the target state return /*featureFlags.isFeatureAEnabled()*/ true? 'featureA' : 'featureB'; } } } }, featureA: { type: 'final', entry: () => console.log('Feature A is enabled') }, featureB: { type: 'final', entry: () => console.log("Feature B is enabled") } } }); let currentState = machine.initialState; currentState = machine.transition(currentState, 'START'); // Navigate based on the feature flag """ ## 3. Scaling XState Applications ### 3.1. Stateless Design **Standard:** Design XState machines to be as stateless as possible. **Do This:** * Store persistent state externally (e.g., in a database or Redis). * Minimize the amount of data stored in the machine's context. Only store necessary data there temporarily. * Favor passing data as events (e.g., "machine.transition(state, { type: 'EVENT', data: { ... } })"). **Don't Do This:** * Store large or frequently changing data directly in the machine's context. * Rely on in-memory state to persist data across restarts. **Why:** Stateless machines are easier to scale horizontally, as any instance can handle any request without relying on local state. ### 3.2. Idempotency **Standard:** Ensure that state transitions are idempotent, meaning they can be applied multiple times without changing the final result. **Do This:** * Design actions and services to be idempotent. * Avoid side effects that depend on the order of transitions. **Don't Do This:** * Perform non-idempotent operations within state transitions. * Assume that transitions will only be applied once. **Why:** Idempotency ensures that state transitions are reliable and can be retried without causing unintended consequences. ### 3.3. Distributed State Management **Standard:** Implement a distributed state management strategy for scaling XState applications across multiple nodes. **Do This:** * Use a shared cache (e.g., Redis, Memcached) to store the machine's context. * Implement a mechanism to synchronize state across nodes (e.g., using pub/sub or a distributed queue). * Consider using an actor model framework (e.g., Akka, Dapr) for managing distributed state. **Don't Do This:** * Rely on local storage or session storage for distributed state management. * Implement complex, custom synchronization logic. **Why:** Distributed state management enables you to scale XState applications horizontally without losing state consistency. ### 3.4 Parallel States and Child Machines **Standard:** Use parallel states and child machines to decompose complex state machines into smaller, more manageable units. **Do This:** * Group related states into parallel states to execute them concurrently. * Encapsulate complex logic into child machines to improve modularity and reusability. * Use the "invoke" property to spawn child machines. **Don't Do This:** * Create monolithic state machines with too many states and transitions. * Overuse parallel states, leading to increased complexity. **Why:** Parallel states and child machines improve the maintainability and scalability of XState applications by breaking them down into smaller, more manageable pieces. **Example (using parallel states):** """javascript import { createMachine, parallel } from 'xstate'; const parallelMachine = createMachine({ id: 'parallelMachine', type: 'parallel', states: { featureA: { initial: 'idle', states: { idle: { on: { START: 'running' }, }, running: { on: { STOP: 'idle' }, }, }, }, featureB: { initial: 'inactive', states: { inactive: { on: { ACTIVATE: 'active' }, }, active: { on: { DEACTIVATE: 'inactive' }, }, }, }, }, }); """ This document serves as a foundation for building scalable, reliable, and maintainable XState-powered systems. It is recommended to periodically review and update these standards as the XState ecosystem evolves and new best practices emerge.
# Security Best Practices Standards for XState This document outlines security best practices for developing applications using XState. It focuses on preventing common vulnerabilities, implementing secure coding patterns, and leveraging security features within the XState ecosystem. These standards are designed to guide developers, be incorporated into AI coding assistants, and promote secure, maintainable, and performant XState applications. ## 1. General Security Principles for State Machines ### 1.1 Input Validation and Sanitization **Do This:** Validate and sanitize all external inputs before using them within your state machine. This includes event data, context data received from APIs, and user inputs. **Don't Do This:** Directly use unsanitized event data in guards, actions, or context updates. This can lead to injection vulnerabilities (e.g., Cross-Site Scripting (XSS), SQL injection). **Why:** Input validation prevents malicious data from impacting the behavior or state of your application, mitigating risks associated with untrusted data sources. **Example:** """typescript import { createMachine } from 'xstate'; interface Context { username: string; } type Event = | { type: 'SUBMIT'; username: string } | { type: 'VALIDATION_ERROR'; message: string }; const sanitizeUsername = (username: string): string => { // Implement your sanitization logic here (e.g., remove HTML tags, escape special characters) return username.replace(/<[^>]*>?/gm, ''); // Basic HTML tag removal }; const myMachine = createMachine<Context, Event>({ id: 'inputValidation', initial: 'idle', context: { username: '' }, states: { idle: { on: { SUBMIT: { target: 'validating', actions: [ (context, event) => { try { if (typeof event.username !== 'string') { throw new Error("Invalid input: Username must be a string."); } const sanitizedUsername = sanitizeUsername(event.username); if (sanitizedUsername.length < 3) { throw new Error("Invalid input: Username must be at least 3 characters."); } context.username = sanitizedUsername; // Safely use the sanitized data } catch (error: any) { return { type: 'VALIDATION_ERROR', message: error.message }; } } ], cond: (context, event) => { try { if (typeof event.username !== 'string') { throw new Error("Invalid input: Username must be a string."); } const sanitizedUsername = sanitizeUsername(event.username); if (sanitizedUsername.length < 3) { throw new Error("Invalid input: Username must be at least 3 characters."); } return true; } catch (error: any) { // Handle validation error return false; } } } } }, validating: { type: 'final', entry: (context) => { console.log("Username: ${context.username}"); } }, } }); //Example usage: const exampleEvent = { type: 'SUBMIT', username: '<script>alert("XSS")</script>JohnDoe' }; myMachine.transition('idle', exampleEvent); """ **Anti-Pattern:** Using "event.data" directly within a context assignment without any validation or sanitization. ### 1.2 Principle of Least Privilege **Do This:** Grant the minimum necessary permissions to the state machine and its associated services. Avoid giving the state machine unrestricted access to sensitive resources. **Don't Do This:** Allow the state machine to have access to resources that exceed its intended operational scope. This limits the potential damage from a compromised state machine. **Why:** Minimizing permissions reduces the attack surface and limits the potential damage in the event of a security breach. **Example:** If a state machine only needs to access a specific API endpoint, configure it to authenticate only to that endpoint, not to the entire API. ### 1.3 Secure Storage of Sensitive Data **Do This:** Encrypt sensitive data stored in the state machine's context, especially if persistence is enabled. Use secure storage mechanisms (e.g., encrypted databases, key vaults) to protect data at rest. **Don't Do This:** Store sensitive data in plain text within the state machine's context or local storage. **Why:** Encryption and secure storage protect sensitive data from unauthorized access. **Example:** """typescript import { createMachine, assign } from 'xstate'; import CryptoJS from 'crypto-js'; // Consider a more robust encryption library interface Context { encryptedApiKey: string; } type Event = { type: 'SET_API_KEY'; apiKey: string }; const ENCRYPTION_KEY = 'YourSecretEncryptionKey'; // Store securely, not in code const encryptApiKey = (apiKey: string): string => { return CryptoJS.AES.encrypt(apiKey, ENCRYPTION_KEY).toString(); }; const decryptApiKey = (encryptedApiKey: string): string => { try { const bytes = CryptoJS.AES.decrypt(encryptedApiKey, ENCRYPTION_KEY); return bytes.toString(CryptoJS.enc.Utf8); } catch (error) { console.error("Decryption failed:", error); return ''; //Or throw an error } }; const myMachine = createMachine<Context, Event>({ id: 'secureStorage', initial: 'idle', context: { encryptedApiKey: '' }, states: { idle: { on: { SET_API_KEY: { actions: assign({ encryptedApiKey: (context, event ) => encryptApiKey(event.apiKey) }) } } }, usingApiKey: { entry: (context) => { const apiKey = decryptApiKey(context.encryptedApiKey); if (apiKey) { // Use the decrypted API key securely console.log('Decrypted API Key:', apiKey); } else { console.error('Failed to decrypt API key.'); } } } } }); //Example usage: const service = myMachine.withContext({ encryptedApiKey: encryptApiKey('MySecureKey')}); service.transition('usingApiKey') """ **Anti-Pattern:** Hardcoding encryption keys directly in the code. Use environment variables or secure key management systems. ### 1.4 Handling Exceptions and Errors Securely **Do This:** Implement proper error handling and logging within your state machine. Avoid exposing sensitive information in error messages. Log errors securely and monitor regularly. Use "try...catch" blocks in actions where external requests happen. **Don't Do This:** Display detailed error messages directly to the user, potentially leaking sensitive information about the system's internals. **Why:** Secure error handling prevents information leakage and helps identify potential security vulnerabilities. **Example:** """typescript import { createMachine, assign } from 'xstate'; interface Context { error: string | null; } type Event = { type: 'FETCH_DATA' }; const myMachine = createMachine<Context, Event>({ id: 'secureErrorHandling', initial: 'idle', context: { error: null }, states: { idle: { on: { FETCH_DATA: { target: 'fetching', } } }, fetching: { invoke: { id: 'fetchData', src: async () => { try { const response = await fetch('/api/sensitive-data'); if (!response.ok) { // Log the error securely (e.g., to a logging service) console.error('API Error:', response.status, response.statusText); throw new Error('Failed to fetch data'); } return await response.json(); } catch (error: any) { //Securely handle exceptions console.error('Fetch Error:', error); throw error; } }, onDone: 'success', onError: { target: 'failure', actions: assign({ error: (context, event) => { // Avoid exposing sensitive details return 'An error occurred while fetching data.'; } }) } } }, success: { type: 'final', entry: () => console.log("Success!") }, failure: { type: 'final', entry: (context) => { console.log("Final error:", context.error) } } } }); //Example usage: const service = myMachine.withContext({ error: null}); service.transition('FETCH_DATA') """ **Anti-Pattern:** Including stack traces or database connection strings in error messages displayed to the user. ### 1.5 Monitoring and Auditing **Do This:** Implement monitoring and auditing mechanisms to track the state machine's activity, identify suspicious behavior, and detect potential security incidents. **Don't Do This:** Operate the state machine without logging activity or implementing alerting rules for unexpected state transitions or errors. **Why:** Monitoring and auditing provide visibility into the state machine's operation and help detect and respond to security threats. **Example:** Log every state transition, event received, and action executed by the state machine to a secure logging system. Set up alerts for unusual patterns, such as rapid state transitions or frequent error events. ## 2. XState-Specific Security Considerations ### 2.1 Secure Service Implementations **Do This:** Securely implement services invoked by the state machine. Use secure communication protocols (HTTPS), validate service responses, and handle errors gracefully. **Don't Do This:** Directly embed insecure service integrations within the state machine, potentially exposing vulnerabilities to third-party systems. **Why:** Secure service implementations ensure that the state machine interacts with external systems in a safe and reliable manner. **Example:** """typescript import { createMachine, assign } from 'xstate'; interface Context { data: any; error: string | null; } type Event = { type: 'FETCH' }; const fetchDataSecurely = async (): Promise<any> => { try { const response = await fetch('https://api.example.com/data', { // Use HTTPS headers: { 'Authorization': 'Bearer mySecureToken' // Securely manage API keys } }); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); // Validate the data structure and content if (!data || typeof data !== 'object') { throw new Error('Invalid data format received from API.'); } return data; } catch (error: any) { console.error('Secure Fetch Error:', error); throw error; //Let the machine handle the error } }; const dataFetchingMachine = createMachine<Context, Event>({ id: 'dataFetching', initial: 'idle', context: { data: null, error: null }, states: { idle: { on: { FETCH: 'fetching' } }, fetching: { invoke: { id: 'fetchData', src: fetchDataSecurely, onDone: { target: 'success', actions: assign({ data: (context, event) => event.data, error: null }) }, onError: { target: 'failure', actions: assign({ error: (context, event) => "Failed to fetch data: ${event.data.message}" //Sanitize error messages here as well }) } } }, success: { type: 'final', entry: (context) => console.log('Data fetched:', context.data) }, failure: { type: 'final', entry: (context) => console.error('Fetch failed:', context.error) } } }); //Example usage: const service = dataFetchingMachine.withContext({ data: null, error: null}); service.transition('FETCH') """ **Anti-Pattern:** Using plain HTTP for service communication or skipping data validation on service responses. ### 2.2 Secure Context Management **Do This:** Manage the state machine's context securely. Avoid storing sensitive information in the root context if possible. When you must store sensitive information, encrypt it and protect it with fine-grained access control. **Don't Do This:** Expose the entire state machine context to untrusted components or directly use it to render UI elements without sanitization. **Why:** Secure context management ensures that sensitive data is protected and not inadvertently exposed. ### 2.3 Preventing Replay Attacks **Do This:** If the state machine handles critical state transitions or financial transactions, implement measures to prevent replay attacks. This can include using nonces, timestamps, or sequence numbers to ensure that each event is processed only once. **Don't Do This:** Allow events to be replayed without any validation, potentially leading to unauthorized state changes or duplicate transactions. **Why:** Preventing replay attacks ensures the integrity of critical state transitions and protects against malicious manipulation. ### 2.4 Guard Function Security **Do This:** Ensure that guard functions that authorize a state transition are secure and cannot be bypassed. Properly validate tokens. A compromised guard function is as bad as unprotected data. **Don't Do This:** Create guard functions that can be easily manipulated. **Why:** Secure guard functions are as important as protecting against XSS attacks. ## 3. Modern Approaches and Patterns ### 3.1 Immutable Context Updates **Do This:** Use immutable data structures for the state machine's context. This prevents accidental modifications and ensures predictable behavior. **Don't Do This:** Directly mutate the context object, potentially leading to unexpected side effects and security vulnerabilities. **Why:** Immutable data structures simplify reasoning about state transitions and prevent accidental data corruption. """typescript import { createMachine, assign } from 'xstate'; import { produce } from 'immer'; // Install immer: npm install immer interface Context { user: { id: string; name: string; }; } type Event = { type: 'UPDATE_NAME'; name: string }; const myMachine = createMachine<Context, Event>({ id: 'immutableContext', initial: 'idle', context: { user: { id: '123', name: 'John Doe' } }, states: { idle: { on: { UPDATE_NAME: { actions: assign( (context, event) => produce(context, draft => { draft.user.name = event.name; }) ) } } } } }); //Example usage: const service = myMachine.withContext({ user: { id: '123', name: 'John Doe' }}); service.transition('UPDATE_NAME', { type: 'UPDATE_NAME', name: 'Jane Doe' }); """ **Anti-Pattern:** Directly modifying "context.user.name = event.name" without using immutable update techniques. ### 3.2 Using JWTs for State Transitions **Do This:** When triggering state transitions based on user actions, use JSON Web Tokens (JWTs) to securely verify the user's identity and permissions. **Don't Do This:** Directly trust user input without proper authentication and authorization. **Why:** JWTs provide a secure and tamper-proof way to verify user identities and control access to state transitions. Using JWTs should also be used for data integrity when manipulating the state itself using guards and actions. ## 4. Conclusion By adhering to these security best practices, developers can build more secure and reliable applications with XState. This document serves as a foundation for secure XState development, emphasizing the importance of input validation, secure storage, proper error handling, and proactive monitoring. Regularly review and update these standards to adapt to evolving security threats and best practices.
# State Management Standards for XState This document outlines the coding standards for state management using XState. It covers approaches to managing application state, data flow, and reactivity within XState machine definitions and implementations. These standards are geared toward the latest XState version and emphasize maintainability, performance, and security. ## 1. State Machine Definition Principles ### 1.1 Explicit State Definitions **Standard:** States should be explicitly declared with meaningful names and clear transitions. Avoid relying on implicit state transitions or string-based state comparisons outside the machine definition. **Do This:** """typescript import { createMachine } from 'xstate'; const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected' } } }, selected: { on: { CONFIRM: { target: 'confirmed' }, CANCEL: { target: 'idle' } } }, confirmed: { type: 'final' } } }); """ **Don't Do This:** """typescript // Anti-pattern: Relying on string comparisons outside the machine if (currentState.value === 'idle') { // ... } """ **Why:** Explicit state definitions improve readability and make it easier to understand the flow of the application. They also allow XState's tooling (Stately Studio, visualizer) to provide better insights and validation. ### 1.2 Hierarchical (Nested) States **Standard:** Use hierarchical (nested) states to represent complex state logic and encapsulate related states. This avoids overly complex flat state machines. **Do This:** """typescript import { createMachine } from 'xstate'; const lightMachine = createMachine({ id: 'light', initial: 'inactive', states: { inactive: { on: { POWER: { target: 'active' } } }, active: { initial: 'green', states: { green: { on: { TIMER: { target: 'yellow' } } }, yellow: { on: { TIMER: { target: 'red' } } }, red: { on: { TIMER: { target: 'green' } } } }, on: { POWER: { target: 'inactive' } } } } }); """ **Don't Do This:** """typescript // Anti-pattern: Flattening all states into a single level const lightMachine = createMachine({ id: 'light', initial: 'inactive', states: { inactive: { /* ... */ }, active_green: { /* ... */ }, active_yellow: { /* ... */ }, active_red: { /* ... */ } } }); """ **Why:** Hierarchical states provide better organization and reduce complexity. They are especially useful for modeling features with their own lifecycles. ### 1.3 Parallel States **Standard:** Use parallel states to model independent concurrent processes within the application. **Do This:** """typescript import { createMachine } from 'xstate'; const multiProcessMachine = createMachine({ id: 'multiProcess', type: 'parallel', states: { processA: { initial: 'idle', states: { idle: { on: { START: { target: 'running' } } }, running: { on: { STOP: { target: 'idle' } } } } }, processB: { initial: 'stopped', states: { stopped: { on: { START: { target: 'running' } } }, running: { on: { STOP: { target: 'stopped' } } } } } } }); """ **Why:** Parallel states are essential when dealing with independent stateful logic occuring concurrently. They ensure that these processes don't interfere with each other's state. ### 1.4 Final States **Standard:** Use final states to represent the end of a specific process or lifecycle within the machine. These states can trigger "done.stateName" events, facilitating coordination between machines. **Do This:** """typescript import { createMachine } from 'xstate'; const orderMachine = createMachine({ id: 'order', initial: 'pending', states: { pending: { on: { PAY: { target: 'processing' } } }, processing: { on: { SUCCESS: { target: 'fulfilled' }, FAILURE: { target: 'rejected' } } }, fulfilled: { type: 'final' }, rejected: { type: 'final' } } }); """ **Why:** Final states clearly mark the completion of a process and serve as important signals for parent machines to react to. ## 2. Data Management (Context) ### 2.1 Context Initialization **Standard:** Initialize the machine context with sensible default values. This prevents unexpected errors when accessing context properties before they are explicitly set. **Do This:** """typescript import { createMachine } from 'xstate'; interface Context { count: number; userName: string | null; } const counterMachine = createMachine<Context>({ id: 'counter', initial: 'idle', context: { count: 0, userName: null }, states: { idle: { on: { INC: { actions: 'increment' } } } }, actions: { increment: (context) => { context.count += 1; } } }); """ **Don't Do This:** """typescript // Anti-pattern: Leaving context undefined import { createMachine } from 'xstate'; interface Context { count?: number; //Optional is BAD userName?: string | null; //Optional is BAD } const counterMachine = createMachine<Context>({ id: 'counter', initial: 'idle', context: {}, //BAD! The machine may crash if "context.count" is accessed before being defined states: { idle: { on: { INC: { actions: 'increment' } } } }, actions: { increment: (context) => { context.count += 1; //Potential Error! "context.count" might be undefined } } }); """ **Why:** Initializing context prevents "undefined" errors and improves type safety. It also clearly communicates the expected structure of the context. ### 2.2 Immutability **Standard:** Treat the context as immutable. Do not directly modify context properties within actions. Instead, return a partial context update from the "assign" action. **Do This:** """typescript import { createMachine, assign } from 'xstate'; interface Context { count: number; } const counterMachine = createMachine<Context>({ id: 'counter', initial: 'idle', context: { count: 0 }, states: { idle: { on: { INC: { actions: 'increment' } } } }, actions: { increment: assign({ count: (context) => context.count + 1 }) } }); """ **Don't Do This:** """typescript // Anti-pattern: Directly modifying the context import { createMachine } from 'xstate'; interface Context { count: number; } const counterMachine = createMachine<Context>({ id: 'counter', initial: 'idle', context: { count: 0 }, states: { idle: { on: { INC: { actions: 'increment' } } } }, actions: { increment: (context) => { context.count += 1; //BAD: Mutates Context! } } }); """ **Why:** Immutability ensures predictable state transitions and simplifies debugging. It also optimizes performance by allowing XState to efficiently detect changes. Using "assign" triggers the proper notifications and re-renders in connected components. ### 2.3 Context Typing **Standard:** Use TypeScript to define the structure of the context. This enforces type safety and helps prevent runtime errors related to incorrect data types. **Do This:** """typescript import { createMachine } from 'xstate'; interface Context { userName: string; age: number; isActive: boolean; } const userMachine = createMachine<Context>({ id: 'user', initial: 'active', context: { userName: 'John Doe', age: 30, isActive: true }, states: { active: { // ... } } }); """ **Why:** Proper typing catches errors early, improves code maintainability, and facilitates refactoring. ## 3. Actions and Effects ### 3.1 Action Naming **Standard:** Use descriptive names for actions that clearly indicate their purpose. **Do This:** """typescript // Clear and descriptive action names const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected', actions: 'loadAvailableRooms' } } }, selected: { //... entry: 'sendConfirmationEmail' } }, actions: { loadAvailableRooms: () => { /* ... */ }, sendConfirmationEmail: () => { /* ... */ } } }); """ **Don't Do This:** """typescript // Anti-pattern: Vague action names const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected', actions: 'action1' } } }, selected: { //... entry: 'action2' } }, actions: { action1: () => { /* ... */ }, action2: () => { /* ... */ } } }); """ **Why:** Descriptive action names make the machine definition easier to understand and maintain. ### 3.2 Side Effects **Standard:** Isolate side effects within actions or services. Avoid performing side effects directly within guards, as guards should be pure functions. **Do This:** """typescript // Correct: Side effect in an action import { createMachine, assign } from 'xstate'; interface Context { orderId: string | null; } const orderMachine = createMachine<Context>({ id: 'order', initial: 'idle', context: { orderId: null }, states: { idle: { on: { CREATE_ORDER: { target: 'creating', actions: 'createOrder' } } }, creating: { invoke: { src: 'createOrderService', onDone: { target: 'active', actions: 'assignOrderId' }, onError: { target: 'failed'} } }, active: { }, failed: {} }, actions: { assignOrderId: assign({ orderId: (context, event) => event.data.orderId //Assuming createOrderService returns { orderId: string } }) }, services: { createOrderService: async () => { //Async function MUST be performed here const response = await fetch('/api/create-order', {method: 'POST'}); return await response.json(); } } }); """ **Don't Do This:** """typescript // Anti-pattern: Side effect in a guard const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected', cond: () => { // BAD! Side effect in a guard. console.log('Checking availability'); return true; } } } } } }); """ **Why:** Isolating side effects makes the code easier to test and reason about. Guards should only perform pure checks on the context and event data. Place asynchronous functions in "services" blocks. ### 3.3 Reusability **Standard:** Define reusable actions and services that can be used across multiple machines or states. Avoid duplicating logic within the machine definition. **Do This:** """typescript // Reusable service definition const sendEmail = async (context, event) => { // ... send email logic }; const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { //... }, invoke: { id: 'emailConfirmation', src: sendEmail, onDone: { target: 'confirmed'} } }); const userMachine = createMachine({ id: 'user', initial: 'active', states: { //... }, invoke: { id: 'welcomeEmail', src: sendEmail, onDone: { target: 'active'} } }); """ **Why:** Reusability reduces code duplication and improves maintainability. Services are best declared outside the machine definition, promoting separation of concerns, and enabling easy unit testing. ### 3.4 Error Handling **Standard:** Implement robust error handling for services. Use the "onError" transition to handle errors and transition the machine to an error state. **Do This:** """typescript import { createMachine } from 'xstate'; const paymentMachine = createMachine({ id: 'payment', initial: 'idle', states: { idle: { on: { PAY: { target: 'processing' } } }, processing: { invoke: { src: 'processPayment', onDone: { target: 'success' }, onError: { target: 'failure', actions: 'logError' } } }, success: { type: 'final' }, failure: { on: { RETRY: { target: 'processing' } } } }, actions: { logError: (context, event) => { console.error('Payment failed:', event.data); } }, services: { processPayment: async (context, event) => { // ... throw new Error('Payment processing failed'); // Simulate an error } } }); """ **Why:** Proper error handling ensures the application gracefully handles unexpected situations and provides useful feedback to the user. ### 3.5 Event Handling **Standard:** Use "raise" to send internal events within the machine that can trigger transitions or actions. This promotes better encapsulation and prevents external components from directly manipulating the machine's state. """typescript import { createMachine, raise } from 'xstate'; const timerMachine = createMachine({ id: 'timer', initial: 'running', context: { remaining: 10 }, states: { running: { after: { 1000: { actions: [ 'decrement', raise({ type: 'TICK' }) ] } }, on: { TICK: { actions: (context) => { if (context.remaining <= 0) { return raise({type: 'TIMEOUT'}) } } }, TIMEOUT: {target: 'expired'} } }, expired: {type: 'final'} }, actions: { decrement: (context) => { context.remaining-- } } }); """ **Why:** "raise" ensures that state transitions are triggered by the machine itself, maintaining control over its internal logic. "raise" can conditionally dispatch different events in Actions. ## 4. Guards (Conditions) ### 4.1 Pure Functions **Standard:** Guards must be pure functions. They should only depend on the context and event data and should not have any side effects. **Do This:** """typescript import { createMachine } from 'xstate'; interface Context { age: number; } const userMachine = createMachine<Context>({ id: 'user', initial: 'inactive', context: { age: 25 }, states: { inactive: { on: { ACTIVATE: { target: 'active', cond: (context) => context.age >= 18 } } }, active: { /* ... */ } } }); """ **Don't Do This:** """typescript // Anti-pattern: Guard with side effect const userMachine = createMachine({ id: 'user', initial: 'inactive', states: { inactive: { on: { ACTIVATE: { target: 'active', cond: (context) => { console.log('Checking age'); // Side effect! return context.age >= 18; } } } }, active: { /* ... */ } } }); """ **Why:** Pure guards ensure predictable behavior and make the machine easier to test. Side effects in guards can lead to unexpected state transitions and make debugging difficult. ### 4.2 Guard Naming **Standard:** Use descriptive names for guards that clearly indicate the condition being evaluated. **Do This:** """typescript const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected', cond: 'isRoomAvailable' } } } }, guards: { isRoomAvailable: (context, event) => { //Implementation of availability check return true; } } }); """ **Don't Do This:** """typescript // Anti-pattern: Vague guard name const bookingMachine = createMachine({ id: 'booking', initial: 'idle', states: { idle: { on: { SELECT: { target: 'selected', cond: 'condition1' } } } }, guards: { condition1: (context, event) => { // ... return true; } } }); """ **Why:** Descriptive guard names improve readability. ## 5. Invoked Services (Actors) ### 5.1 Service Definition **Standard:** Define services in the "invoke" property of a state. Use the "src" property to specify the service's implementation. **Do This:** """typescript import { createMachine } from 'xstate'; const dataFetchMachine = createMachine({ id: 'dataFetch', initial: 'idle', states: { idle: { on: { FETCH: { target: 'loading' } } }, loading: { invoke: { id: 'fetchData', src: 'fetchDataService', onDone: { target: 'success', actions: 'assignData' }, onError: { target: 'failure', actions: 'assignError' } } }, success: { /* ... */ }, failure: { /* ... */ } }, actions: { assignData: assign({ data: (context, event) => event.data }), assignError: assign({ error: (context, event) => event.data }) }, services: { fetchDataService: async (context, event) => { const response = await fetch('/api/data'); return await response.json(); } } }); """ **Why:** This structure clearly defines the service and its lifecycle within the state machine. ### 5.2 Actor Communication **Standard:** Use the "send" action or the "sendParent" to communicate with invoked actors (services) or the parent machine, respectively. Ensure there is a clear protocol for communication between actors and their parent machines. **Do This:** """typescript // Example: Sending an event to a child service. import { createMachine, send } from 'xstate'; const childMachine = createMachine({ id: 'child', initial: 'idle', states: { idle: { on: { REQUEST_DATA: { target: 'loading' } } }, loading: { after: { 5000: {target: 'idle'} }, on: { DATA_RECEIVED: { target: 'idle' }, TIMEOUT: { target: 'idle' } } } } }); const parentMachine = createMachine({ id: 'parent', initial: 'active', states: { active: { invoke: { id: 'childService', src: childMachine }, entry: send({ type: 'REQUEST_DATA' }, { to: 'childService' }), on: { '*': { actions: (context, event) => { console.log('Parent received:', event.type); } } } } } } ); """ **Why:** Clear communication protocols are essential for managing complex interactions between machines. Use explicit event types and data structures. Avoid tightly coupled machine definition. ### 5.3 Cleanup **Standard:** Implement cleanup logic within services to release resources or cancel ongoing operations when the service is interrupted or completes. Use the "onDone" and "onError" transitions to perform cleanup actions. **Example:** Leveraging the "aborted" signal for cleanup: """typescript import { createMachine, assign } from 'xstate'; const fileUploadMachine = createMachine({ id: 'fileUpload', initial: 'idle', context: { uploadProgress: 0, controller: null }, states: { idle: { on: { UPLOAD: { target: 'uploading', actions: 'createAbortController' } } }, uploading: { invoke: { id: 'uploadFile', src: 'uploadFileService', onDone: 'success', onError: 'failure' }, on: { CANCEL: { target: 'idle', actions: 'abortUpload' } } }, success: { type: 'final' }, failure: { type: 'final' } }, actions: { createAbortController: assign({ controller: () => new AbortController() }), abortUpload: (context) => { context.controller.abort(); } }, services: { uploadFileService: async (context, event) => { const file = event.file; const controller = context.controller; const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData, signal: controller.signal // Pass the abort signal }); return response.json(); } } }); """ **Why:** Proper cleanup avoids memory leaks, prevents resource exhaustion, and ensures the application remains stable. AbortController usage specifically for canceling ongoing asynchronous requests is an important web platform feature. ## 6. Testing ### 6.1 Unit Testing **Standard:** Write unit tests to verify the behavior of individual states, transitions, actions, and guards within the machine. **Example:** """typescript import { interpret } from 'xstate'; import { bookingMachine } from './bookingMachine'; // Assuming bookingMachine is in a separate file describe('bookingMachine', () => { it('should transition from idle to selected on SELECT event', (done) => { const service = interpret(bookingMachine).onTransition((state) => { if (state.value === 'selected') { expect(state.value).toBe('selected'); done(); } }); service.start(); service.send({ type: 'SELECT' }); }); }); """ ### 6.2 Integration Testing **Standard:** Write integration tests to verify the interaction between multiple machines or between the machine and external systems. **Why:** Testing ensures the correct behavior of state machines and helps prevent regressions. Focus on testing state transitions, actions and conditional flows. ## 7. Tooling & Conventions ### 7.1 Stately Studio **Standard:** Use Stately Studio (stately.ai) to visually design, analyze, and test state machines. Leverage Stately Studio's code generation capabilities to create XState machine definitions. ### 7.2 Visualizer **Standard:** Use the XState visualizer (https://stately.ai/viz) to visualize the state machine and understand its behavior. Regularly visualize the machine to confirm its design meets the required functionality. ### 7.3 Code Generation **Standard:** When possible, use code generation tools (e.g., Stately Studio) to automatically generate XState machine definitions. This can reduce errors and ensure consistency. ### 7.4 Machine IDs **Standard:** Provide unique and descriptive IDs for all machines. It is MANDATORY to use IDs other invoke calls will fail, and it also supports a more visual and easier debugging journey. """javascript invoke: { id: 'emailConfirmation', // Mandatory, for tracking and linking events src: sendEmail, onDone: { target: 'confirmed'} } """ Adhering to these standards will result in more maintainable, performant, and secure XState applications. This document should be regularly reviewed and updated to reflect the latest best practices and features of XState.