PatternsState and Logic
State Machines Advanced (XState)
Model complex workflows with finite state machines
State machines make impossible states impossible. They're perfect for complex workflows like checkout flows, multi-step forms, and connection management where certain transitions should never happen.
Why State Machines?
// ❌ BAD: Boolean soup (impossible states possible)
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// Can be loading AND error AND success at once!
// ✅ GOOD: State machine (one state at a time)
type State = 'idle' | 'loading' | 'success' | 'error';
const [state, setState] = useState<State>('idle');XState Setup
npm install xstate @xstate/reactBasic Machine
// machines/fetchMachine.ts
import { createMachine, assign } from 'xstate';
interface FetchContext {
data: any;
error: string | null;
}
type FetchEvent =
| { type: 'FETCH' }
| { type: 'RETRY' };
export const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: {
data: null,
error: null,
} as FetchContext,
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({
data: (_context, event) => event.data,
}),
},
onError: {
target: 'failure',
actions: assign({
error: (_context, event) => event.data,
}),
},
},
},
success: {
on: {
FETCH: 'loading',
},
},
failure: {
on: {
RETRY: 'loading',
},
},
},
});Using in React
// components/DataFetcher.tsx
import { useMachine } from '@xstate/react';
import { fetchMachine } from '@/machines/fetchMachine';
export function DataFetcher() {
const [state, send] = useMachine(fetchMachine, {
services: {
fetchData: async () => {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
},
},
});
return (
<div>
{state.matches('idle') && (
<button onClick={() => send('FETCH')}>Fetch Data</button>
)}
{state.matches('loading') && <div>Loading...</div>}
{state.matches('success') && (
<div>
<p>Data: {JSON.stringify(state.context.data)}</p>
<button onClick={() => send('FETCH')}>Refresh</button>
</div>
)}
{state.matches('failure') && (
<div>
<p>Error: {state.context.error}</p>
<button onClick={() => send('RETRY')}>Retry</button>
</div>
)}
</div>
);
}Multi-Step Form Machine
// machines/formMachine.ts
import { createMachine, assign } from 'xstate';
interface FormContext {
step1Data: { name: string; email: string } | null;
step2Data: { address: string; city: string } | null;
step3Data: { payment: string } | null;
}
export const formMachine = createMachine({
id: 'form',
initial: 'step1',
context: {
step1Data: null,
step2Data: null,
step3Data: null,
} as FormContext,
states: {
step1: {
on: {
NEXT: {
target: 'step2',
actions: assign({
step1Data: (_context, event) => event.data,
}),
},
},
},
step2: {
on: {
NEXT: {
target: 'step3',
actions: assign({
step2Data: (_context, event) => event.data,
}),
},
BACK: 'step1',
},
},
step3: {
on: {
SUBMIT: 'submitting',
BACK: 'step2',
},
},
submitting: {
invoke: {
src: 'submitForm',
onDone: 'success',
onError: 'error',
},
},
success: {
type: 'final',
},
error: {
on: {
RETRY: 'submitting',
BACK: 'step3',
},
},
},
});Guards (Conditional Transitions)
export const authMachine = createMachine({
id: 'auth',
initial: 'loggedOut',
context: {
attempts: 0,
maxAttempts: 3,
},
states: {
loggedOut: {
on: {
LOGIN: 'loggingIn',
},
},
loggingIn: {
invoke: {
src: 'login',
onDone: {
target: 'loggedIn',
actions: assign({ attempts: 0 }),
},
onError: [
{
target: 'locked',
cond: 'maxAttemptsReached',
},
{
target: 'loggedOut',
actions: assign({
attempts: (context) => context.attempts + 1,
}),
},
],
},
},
loggedIn: {
on: {
LOGOUT: 'loggedOut',
},
},
locked: {
after: {
60000: 'loggedOut', // Unlock after 1 minute
},
},
},
}, {
guards: {
maxAttemptsReached: (context) => context.attempts >= context.maxAttempts - 1,
},
});Parallel States
// Multiple states active simultaneously
export const appMachine = createMachine({
id: 'app',
type: 'parallel',
states: {
auth: {
initial: 'loggedOut',
states: {
loggedOut: { on: { LOGIN: 'loggedIn' } },
loggedIn: { on: { LOGOUT: 'loggedOut' } },
},
},
theme: {
initial: 'light',
states: {
light: { on: { TOGGLE_THEME: 'dark' } },
dark: { on: { TOGGLE_THEME: 'light' } },
},
},
notifications: {
initial: 'enabled',
states: {
enabled: { on: { DISABLE: 'disabled' } },
disabled: { on: { ENABLE: 'enabled' } },
},
},
},
});Hierarchical (Nested) States
export const editorMachine = createMachine({
id: 'editor',
initial: 'reading',
states: {
reading: {
on: {
EDIT: 'editing',
},
},
editing: {
initial: 'normal',
states: {
normal: {
on: {
BOLD: 'bold',
ITALIC: 'italic',
},
},
bold: {
on: {
NORMAL: 'normal',
ITALIC: 'boldItalic',
},
},
italic: {
on: {
NORMAL: 'normal',
BOLD: 'boldItalic',
},
},
boldItalic: {
on: {
NORMAL: 'normal',
},
},
},
on: {
SAVE: 'saving',
CANCEL: 'reading',
},
},
saving: {
invoke: {
src: 'saveDocument',
onDone: 'reading',
onError: 'editing',
},
},
},
});Visualization
// Visualize state machines at stately.ai/viz
import { createBrowserInspector } from '@stately/inspect';
const inspector = createBrowserInspector();
const [state, send] = useMachine(machine, {
inspect: inspector.inspect,
});Testing
import { interpret } from 'xstate';
import { fetchMachine } from './fetchMachine';
describe('fetchMachine', () => {
it('transitions from idle to loading', () => {
const service = interpret(fetchMachine).start();
expect(service.state.value).toBe('idle');
service.send('FETCH');
expect(service.state.value).toBe('loading');
});
});Best Practices
- Model the domain: Match real-world states
- Use guards: Conditional transitions
- Actions for side effects: assign, log, etc.
- Services for async: invoke for API calls
- Visualize: Use XState Visualizer
- Test states: Verify impossible states can't happen
- Type context: Full TypeScript support
- Document transitions: Clear event names
- Keep machines small: Compose machines
- Use actors: For spawning child machines
State machines eliminate entire classes of bugs by making impossible states impossible!