Machine — States and Transitions
2.04 kB minified + gzipped
An upload screen lets the operator cancel while the request is in flight. The response arrives afterwards and the handler stores it, so the cancelled file reappears as uploaded. The same gap shows up wherever a screen’s behaviour depends on which mode it is currently in, and the conditional checks scatter across the component.
Core Behavior
Section titled “Core Behavior”Machine<States, Events> is a transition table that maps each state kind to the events it handles
and the next state each handler returns.
- A handler returns the next state; an event without a handler leaves the current state unchanged.
Machine.interpretcreates an actor that runs entry effects when a state is entered and aborts the previous entry’s signal when the state changes.- A state change also runs the previous entry’s cleanup, then its exit hook, notifies subscribers, and enters the new state.
sendqueues re-entrant events and processes them in order;abortstops the actor and runs the active cleanup.
Reach for Machine when handling an event depends on the state the actor is in. A plain
EventBus broadcasts messages without state, so every subscriber sees every
matching event. The type model covers definitions and instances, and the
choosing guide places the execution shapes side by side.
Example: File Upload
Section titled “Example: File Upload”An upload screen can be idle, uploading, complete, or failed. If the user cancels an upload, a late response from that upload should not replace the current state.
When an actor receives an event, it checks the current state’s handler. A handler returning the same state object leaves the lifecycle alone; a different state object triggers exit and entry:
flowchart TD
Event["Actor receives event"] --> Handler{"Handler returns a different state object?"}
Handler -->|No handler or same object| Same["Keep current state and lifecycle"]
Handler -->|Yes| Exit["Abort signal, run cleanup and exit"]
Exit --> Notify["Store next state and notify subscribers"]
Notify --> Entry["Run next state's entry"]
Managing an Upload
Section titled “Managing an Upload”The upload state starts a request on entry. The actor passes the signal to fetch, and a response
sends either COMPLETE or FAIL. CANCEL returns to idle while the request is pending:
The actor’s guarded send prevents an old entry from updating the new state even if the underlying
work ignores its signal. Events accepted by an entry stay queued and are processed in order; the
state at each event’s turn decides the transition. To use a machine as a reducer without entry or
exit effects, call Machine.transition or Machine.to.reducer instead.
Feeding the Actor from a Bus
Section titled “Feeding the Actor from a Bus”When the commands arrive from elsewhere in the application, subscribe the actor to those kinds. The
subscription forwards only START and CANCEL; completion is reported by the entry effect that
started the request, through its own send:
Machine.interpret also accepts the bus directly as bus, which forwards every kind and lets
unhandled events leave the state unchanged. See EventBus for forwarding and
sequence matching.
Core Operations
Section titled “Core Operations”Defining and Inspecting
Section titled “Defining and Inspecting”| Signature | What it does | Return type or behavior |
|---|---|---|
Machine.state(kind, value?) |
Creates a state, optionally with a payload. | A state with a kind discriminator. |
Machine.make(states) |
Defines a state transition table. | Machine<States, Events> |
Machine.is(kind) |
Checks a state’s kind. | A curried type guard. |
Machine.match(state, cases) |
Handles each state kind and infers the return union. | The matching handler’s result. |
Machine.match<States, R>(cases) |
Curried form for pipe; requires explicit types. |
The matching handler’s result. |
Transitioning and Running
Section titled “Transitioning and Running”| Signature | What it does | Behavior |
|---|---|---|
Machine.transition(machine, event) |
Computes the next state from a supplied state. | Does not run entry or exit effects. |
Machine.to.reducer(machine) |
Creates a (state, event) => state reducer. |
Pass the initial state to useReducer. |
Machine.interpret(machine, options) |
Starts a stateful actor and runs the initial entry. | Options include initial, id, and bus. |
actor.send(event) / getState() |
Sends an event / reads the current state. | Re-entrant sends are queued. |
actor.subscribe(fn) |
Observes state changes. | Returns an unsubscribe function. |
actor.abort() / isRunning() |
Stops the actor / checks whether it is running. | Abort runs active cleanup and exit. |
Machine.State<Kind, Data, AllowedTargets> restricts which state kinds a transition handler may
return; returning the current state is always allowed. Machine.is is curried:
Machine.is("uploading")(state). The data-first Machine.match infers payloads and the union of
branch results; the curried form needs both type arguments because it cannot see the state value.
Machine.transition(machine, event)(state) is data-last. Machine.to.reducer is uncurried and fits
useReducer directly. Machine.interpret runs the initial entry immediately, so an entry effect
that fetches starts before the first send.
Failure Behavior and Edge Cases
Section titled “Failure Behavior and Edge Cases”An unhandled event returns the same state, and the actor uses object identity to detect a change:
returning the original object skips lifecycle hooks and subscribers, while a new object triggers
them even when its kind is unchanged. Machine.transition lets an exception from a handler escape
immediately. actor.send rethrows synchronous exceptions from transitions, entry, exit, cleanup,
and listeners after the transition or cleanup in progress completes, so the actor is not left
half-way through a state change.
Callbacks must handle expected failures themselves. Use Result for synchronous work and
Task.Result for asynchronous work. Handle rejected promises inside an async entry effect, as in
the upload example: the actor does not observe a promise returned from an entry, and a rejection is
unhandled. subscribe reports subsequent changes only; read getState() for the initial state.
After actor.abort(), later events are ignored, the active cleanup and exit hook run, and
getState() keeps the last state. Aborting is local to the actor and returns no result; it does not
produce the shared Aborted outcome used by engines that settle one. send
called from a transition, an entry or exit effect, or a subscriber while another event is processing
queues the event and returns the current state immediately; the queued event is processed after the
current one finishes.
The optional bus must share at least one event kind with the machine, and the shared payloads must
be compatible; that requirement is checked by the compiler. Kinds the machine does not declare are
delivered to send and leave the state unchanged. If the initial entry throws, the actor does not
start, its bus subscription is removed, and the error is rethrown.