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