Why Are Event and Action Different Types?
It is tempting to think an Event and an Action are the same kind of thing — both are "stuff that happens around a transition." They are not, and conflating them is the most common source of confusion. They are opposite ends of one edge:
| Event | Action |
|---|---|
Input — arrives from the outside world. |
Output — emitted by the machine. |
You push it: |
You receive it: |
The cause ("payment succeeded"). |
The effect ("charge the card", "ship the parcel"). |
Often a bare signal with no data. |
Usually carries data the downstream doer needs. |
Past tense — it already happened. |
Imperative — go do this. |
A quick test when you are unsure which one you are holding: ask who produced it. If the outside world produced it (a callback fired, a timer elapsed, a user clicked), it’s an Event. If the machine produced it as a consequence of moving, it’s an Action.
What the separation buys you
Keeping them as separate type parameters buys real things:
-
Compile-time direction safety.
transition(EventT)cannot accept anAction, so you can never accidentally feed an output back in as an input. -
Different shapes. An event like
PaymentSucceededis a bare signal; the action it triggers,ShipParcel(address), needs a payload — and that payload often comes from the State, not the event (the ★ edge in the example). Forcing both into one type would lose that. -
Decoupling decision from doing. The machine decides and emits a command — it never runs the command itself. Some other code (the call site, a listener, or a worker on another host) interprets the action and performs it (see Handling Actions). That separation keeps the machine pure and is what makes the async pattern possible — the machine never blocks on the side effect.
The Action payload is optional. A transition that emits nothing — sb.transitionTo(nextState) — is perfectly valid (the PaymentFailed → Cancelled edge). If your machine never emits actions, give ActionT a wildcard: Hydra<MyState, MyEvent, ?>.
|