Skip to main content

What are events?

Events are objects that trigger transitions in state machines. They represent things that happen - user actions, system notifications, timer expirations, or any other occurrence that the machine should respond to.
All events in XState must be objects with a type property.

Event structure

The simplest event:
{ type: 'SUBMIT' }
Events with payload data:
{
  type: 'UPDATE_USER',
  name: 'Alice',
  email: 'alice@example.com'
}

Sending events

Send events to actors using send():
import { createMachine, createActor } from 'xstate';

const machine = createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        START: 'running'
      }
    },
    running: {
      on: {
        STOP: 'idle'
      }
    }
  }
});

const actor = createActor(machine);
actor.start();

// Send events
actor.send({ type: 'START' });
actor.send({ type: 'STOP' });

TypeScript event types

Define event types for type safety:
import { setup } from 'xstate';

const machine = setup({
  types: {
    events: {} as
      | { type: 'SUBMIT'; data: string }
      | { type: 'CANCEL' }
      | { type: 'UPDATE'; field: string; value: string }
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        SUBMIT: 'processing',
        CANCEL: 'cancelled'
      }
    },
    processing: {},
    cancelled: {}
  }
});
Now TypeScript will enforce correct event structure:
// ✓ Valid
actor.send({ type: 'SUBMIT', data: 'hello' });

// ✗ Type error - missing 'data' property
actor.send({ type: 'SUBMIT' });

// ✗ Type error - unknown event type
actor.send({ type: 'UNKNOWN' });

Accessing event data

Event data is available in actions, guards, and other machine logic:

In actions

import { setup, assign } from 'xstate';

const machine = setup({
  types: {
    context: {} as { message: string },
    events: {} as { type: 'SET_MESSAGE'; text: string }
  }
}).createMachine({
  context: { message: '' },
  initial: 'active',
  states: {
    active: {
      on: {
        SET_MESSAGE: {
          actions: [
            ({ event }) => console.log('Received:', event.text),
            assign({
              message: ({ event }) => event.text
            })
          ]
        }
      }
    }
  }
});

In guards

import { setup } from 'xstate';

const machine = setup({
  types: {
    events: {} as { type: 'SUBMIT'; age: number }
  },
  guards: {
    isAdult: ({ event }) => {
      return event.type === 'SUBMIT' && event.age >= 18;
    }
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        SUBMIT: [
          {
            guard: 'isAdult',
            target: 'adult'
          },
          {
            target: 'minor'
          }
        ]
      }
    },
    adult: {},
    minor: {}
  }
});

In invoked actors

import { setup, fromPromise } from 'xstate';

const fetchData = fromPromise(async ({ input }: { input: { id: string } }) => {
  const response = await fetch(`/api/data/${input.id}`);
  return response.json();
});

const machine = setup({
  types: {
    events: {} as { type: 'FETCH'; id: string }
  },
  actors: {
    fetchData
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        FETCH: 'loading'
      }
    },
    loading: {
      invoke: {
        src: 'fetchData',
        input: ({ event }) => ({ id: event.id }),
        onDone: 'success',
        onError: 'error'
      }
    },
    success: {},
    error: {}
  }
});

Event patterns

Multiple transitions for same event

states: {
  idle: {
    on: {
      CLICK: [
        { guard: 'isDoubleClick', target: 'selected' },
        { target: 'highlighted' }
      ]
    }
  }
}

Wildcard events

Handle any event:
states: {
  active: {
    on: {
      SPECIFIC_EVENT: 'nextState',
      '*': {
        actions: ({ event }) => console.log('Unhandled event:', event.type)
      }
    }
  }
}

Event namespacing

Organize related events:
type Events =
  | { type: 'user.login'; credentials: { email: string; password: string } }
  | { type: 'user.logout' }
  | { type: 'user.update'; data: UserData }
  | { type: 'data.fetch'; endpoint: string }
  | { type: 'data.save'; payload: any };

Built-in events

Done events

Automatically sent when actors complete:
import { setup, fromPromise } from 'xstate';

const fetchUser = fromPromise(async () => {
  const response = await fetch('/api/user');
  return response.json();
});

const machine = setup({
  actors: { fetchUser }
}).createMachine({
  initial: 'loading',
  states: {
    loading: {
      invoke: {
        src: 'fetchUser',
        onDone: {
          target: 'success',
          actions: ({ event }) => {
            console.log('User data:', event.output);
          }
        }
      }
    },
    success: {}
  }
});

Error events

Automatically sent when actors fail:
const machine = setup({
  actors: { fetchUser }
}).createMachine({
  initial: 'loading',
  states: {
    loading: {
      invoke: {
        src: 'fetchUser',
        onError: {
          target: 'error',
          actions: ({ event }) => {
            console.error('Failed:', event.error);
          }
        }
      }
    },
    error: {}
  }
});

Raising events

Raise events internally without external send:
import { setup, raise } from 'xstate';

const machine = setup({
  types: {
    events: {} as
      | { type: 'BUTTON_CLICK' }
      | { type: 'INTERNAL_PROCESS' }
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        BUTTON_CLICK: {
          actions: raise({ type: 'INTERNAL_PROCESS' })
        },
        INTERNAL_PROCESS: 'processing'
      }
    },
    processing: {}
  }
});
Use raise for internal coordination between parts of your machine.

Emitting events

Emit events to parent machines or external observers:
import { setup, emit } from 'xstate';

const childMachine = setup({
  types: {
    events: {} as { type: 'DO_WORK' },
    emitted: {} as { type: 'WORK_DONE'; result: string }
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        DO_WORK: {
          actions: emit({
            type: 'WORK_DONE',
            result: 'completed'
          })
        }
      }
    }
  }
});

Delayed events

Schedule events to be sent after a delay:
import { setup, sendTo } from 'xstate';

const machine = setup({}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        START: {
          target: 'waiting',
          actions: sendTo(
            ({ self }) => self,
            { type: 'TIMEOUT' },
            { delay: 5000 }
          )
        }
      }
    },
    waiting: {
      on: {
        TIMEOUT: 'done'
      }
    },
    done: {}
  }
});
Or use after for cleaner syntax:
const machine = createMachine({
  initial: 'waiting',
  states: {
    waiting: {
      after: {
        5000: 'timeout'
      }
    },
    timeout: {}
  }
});

Event assertions

Assert event types for type narrowing:
import { assertEvent } from 'xstate';

const machine = setup({
  types: {
    events: {} as
      | { type: 'SUBMIT'; data: string }
      | { type: 'CANCEL' }
  },
  actions: {
    handleSubmit: ({ event }) => {
      assertEvent(event, 'SUBMIT');
      // TypeScript now knows event has 'data' property
      console.log(event.data);
    }
  }
}).createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        SUBMIT: {
          actions: 'handleSubmit'
        }
      }
    }
  }
});

Best practices

Use UPPER_CASE for event types to distinguish them from other strings.
Keep event names descriptive and action-oriented: FORM_SUBMITTED, DATA_LOADED, USER_LOGGED_OUT.
Namespace related events: user.login, user.logout, user.update.
Include all relevant data in the event payload rather than relying on external state.
Don’t mutate event objects. They should be treated as immutable.

Next steps

Transitions

Learn how events trigger transitions

Actions

Respond to events with actions

Raise action

Raise internal events

Emit action

Emit events to observers