Understanding Events
Understanding Events
Events are the triggers that cause state transitions. They represent things that happen in your application - user interactions, API responses, timers, system notifications, or any other occurrence that should cause your machine to change state.
In traditional imperative code, you handle events by directly manipulating state variables. With finite state machines, events are sent to the machine, which then determines if and how the state should change based on the current state and defined transitions.
Sending Events
Events are sent to a machine service using the send method:
import { interpret } from 'robot3';
const service = interpret(machine, () => {
console.log('State changed to:', service.machine.current);
});
// Send events to trigger transitions
service.send('fetch');
service.send('success');
service.send('retry');
Event Types
Robot supports several types of events that work together to create responsive state machines.
User Events
User events are explicitly sent via service.send(). These represent direct interactions or manual triggers:
const machine = createMachine({
idle: state(
transition('start', 'running')
),
running: state(
transition('pause', 'paused'),
transition('stop', 'idle')
),
paused: state(
transition('resume', 'running'),
transition('stop', 'idle')
)
});
const service = interpret(machine);
// User clicks a button
button.addEventListener('click', () => {
service.send('start'); // Manually triggered user event
});
Immediate Transitions
Immediate transitions automatically trigger when entering a state, without requiring an explicit event:
import { createMachine, state, transition, immediate } from 'robot3';
const machine = createMachine({
idle: state(
transition('fetch', 'loading')
),
loading: state(
transition('done', 'validate')
),
validate: state(
// Automatically transitions when entering 'validate'
immediate('loaded', guard(isValid)),
immediate('error', guard(isInvalid))
),
loaded: state(),
error: state()
});
Invoked Events
When using invoke for async operations, Robot automatically generates done and error events:
import { createMachine, invoke, state, transition } from 'robot3';
async function fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}
const machine = createMachine({
idle: state(
transition('fetch', 'loading')
),
loading: invoke(fetchUsers,
// 'done' event fires automatically on success
transition('done', 'loaded'),
// 'error' event fires automatically on failure
transition('error', 'error')
),
loaded: state(),
error: state()
});
Event Payloads
Events can carry additional data that’s used by guards, actions, or reducers:
// Simple string event
service.send('increment');
// Event object with data
service.send({
type: 'login',
username: 'alice',
timestamp: Date.now()
});
// Event with complex data
service.send({
type: 'updateUser',
data: {
id: 123,
name: 'Alice',
email: 'alice@example.com'
}
});
Accessing Event Data
Event data is available in guards, actions, and reducers:
import { createMachine, state, transition, guard, reduce } from 'robot3';
const machine = createMachine({
idle: state(
transition('login', 'authenticating',
// Guard can check event data
guard((ctx, ev) => ev.username && ev.password),
// Reducer can use event data
reduce((ctx, ev) => ({
...ctx,
username: ev.username
}))
)
),
authenticating: state()
});
// Send event with data
service.send({
type: 'login',
username: 'alice',
password: 'secret123'
});
Event Naming Conventions
Choose event names that describe what happened, using past tense or present tense:
// ✅ Good - describes what happened
'clicked'
'submitted'
'success'
'error'
'timeout'
'userLoggedIn'
// ❌ Avoid - describes what to do
'goToNextPage'
'shouldLoad'
'handleClick'
'makeRequest'
This keeps your state machine declarative and easier to reason about.
Event Flow
Here’s how events flow through a state machine:
- Event sent:
service.send('eventName')is called - Current state checked: Machine looks at transitions in the current state
- Matching transition found: Machine finds a transition with that event name
- Guards evaluated: If guards exist, they check if transition can proceed
- Actions executed: If guards pass, actions/reducers run
- State changes: Machine transitions to target state
- Listeners notified: Any change listeners are called
const machine = createMachine({
idle: state(
transition('fetch', 'loading',
guard((ctx) => ctx.hasToken),
reduce((ctx) => ({ ...ctx, loading: true }))
)
),
loading: state()
});
// This triggers the entire flow above
service.send('fetch');
Event Queuing
Events are processed synchronously in the order they’re sent. If you send multiple events, they’re handled one at a time:
service.send('first'); // Processed immediately
service.send('second'); // Processed after 'first' completes
service.send('third'); // Processed after 'second' completes
This predictable ordering prevents race conditions and makes debugging easier.
Ignored Events
If an event is sent but there’s no matching transition in the current state, the event is ignored:
const machine = createMachine({
idle: state(
transition('start', 'running')
// No 'stop' transition defined
),
running: state(
transition('stop', 'idle')
)
});
const service = interpret(machine);
// Machine is in 'idle' state
service.send('stop'); // Ignored - no 'stop' transition from 'idle'
service.send('start'); // Works - transitions to 'running'
This is intentional and prevents invalid state transitions. Only events defined in the current state’s transitions can cause state changes.
Event-Driven Architecture
Events make your state machines reactive and decoupled:
// Different parts of your app can send events
button.addEventListener('click', () => {
service.send('userAction');
});
socket.on('message', (data) => {
service.send({ type: 'messageReceived', data });
});
setTimeout(() => {
service.send('timeout');
}, 5000);
// The machine handles all events based on current state
const machine = createMachine({
waiting: state(
transition('userAction', 'processing'),
transition('messageReceived', 'processing'),
transition('timeout', 'expired')
),
processing: state(),
expired: state()
});
Common Event Patterns
Request/Response Pattern
idle: state(
transition('fetch', 'loading')
),
loading: state(
transition('success', 'loaded'),
transition('failure', 'error')
)
// Usage:
service.send('fetch');
// Later, after API call:
service.send({ type: 'success', data: result });
// Or:
service.send({ type: 'failure', error: err });
User Interaction Pattern
closed: state(
transition('open', 'opening')
),
opening: state(
transition('opened', 'open')
),
open: state(
transition('close', 'closing')
),
closing: state(
transition('closed', 'closed')
)
Validation Pattern
editing: state(
transition('submit', 'validating')
),
validating: state(
transition('valid', 'submitting'),
transition('invalid', 'editing')
),
submitting: state(
transition('success', 'submitted'),
transition('error', 'editing')
)
Why Event-Based State Changes Matter
Predictable State Changes
Events make all state changes explicit and traceable:
// Without FSM - state changes are implicit ❌
function onClick() {
isLoading = true;
fetchData().then(() => {
isLoading = false;
hasData = true;
});
}
// With FSM - state changes are explicit ✅
function onClick() {
service.send('fetch');
}
// Machine handles the state changes
loading: invoke(fetchData,
transition('done', 'loaded')
)
Centralized State Logic
All state change logic lives in one place - your machine definition:
const machine = createMachine({
idle: state(
transition('start', 'running')
),
running: state(
transition('pause', 'paused'),
transition('stop', 'idle')
),
paused: state(
transition('resume', 'running'),
transition('stop', 'idle')
)
});
// Any part of your code can send events
service.send('start');
service.send('pause');
service.send('resume');
Decoupled Components
Components don’t need to know about state logic - they just send events:
// Component only sends events
function PlayButton() {
return <button onClick={() => service.send('start')}>Play</button>;
}
function PauseButton() {
return <button onClick={() => service.send('pause')}>Pause</button>;
}
// Machine determines what happens
const machine = createMachine({
stopped: state(transition('start', 'playing')),
playing: state(transition('pause', 'paused')),
paused: state(transition('start', 'playing'))
});
Related Topics
- Transitions - How events trigger state changes
- Guards - Conditional event handling
- Actions - Side effects when events occur
- invoke API - Automatic event generation from async operations
- immediate API - Automatic transitions without explicit events