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