Skip to content

Parallel States

A state can declare parallel regions — a set of independent sub-machines that all become active when the parent state is entered and are destroyed when it is left.

ts
const editor = defineMachine({
  id: 'editor',
  initial: 'editing',
  states: {
    editing: {
      parallel: {
        saving: {
          initial: 'idle',
          states: {
            idle: { on: { START_SAVE: { target: 'saving' } } },
            saving: { on: { SAVE_DONE: { target: 'saved' } } },
            saved: {},
          },
        },
        validation: {
          initial: 'valid',
          states: {
            valid: { on: { INVALIDATE: { target: 'invalid' } } },
            invalid: { on: { VALIDATE: { target: 'valid' } } },
          },
        },
      },
    },
    idle: {},
  },
})

send() delivers every event to all active regions. Each region handles it independently.

ts
const { matches, send } = useMachine(editor)

matches('editing') // main state
matches({ saving: 'idle' }) // region check
matches({ validation: 'valid' }) // another region

await send('INVALIDATE')
matches({ validation: 'invalid' }) // true
matches({ saving: 'idle' }) // still true — unaffected

Context conflict resolution: each region's transition only ever reports the fields its own actions actually touched — an untouched field is never overwritten by a sibling region's stale copy of it. When two regions do return a Partial<context> that touches the same field, the last region in declaration order wins. A console.warn is emitted in dev mode naming the conflicting regions and field.

Limitation: parallel regions support one level of nesting. Regions cannot themselves contain parallel. This is a deliberate choice to control complexity.