Handling an event does not always mean moving to another state. Bare on(Event) accepts the event
at its active source without selecting a target.
Handle an event without leaving the state.
A state may accept an event without selecting another state. Targetless on keeps handling and configuration change as separate choices.
1 · No active handler
The connection has nowhere to receive a heartbeat.
Heartbeat is a valid event value, but Connected declares no behavior for it. Defining the vocabulary still does not make the event globally handled.
import { State, Statechart, States } from "@motive/motive";
import { Schema } from "effect";
export const Heartbeat = Schema.TaggedStruct("Heartbeat", {});
class Connection extends State.Compound<Connection>()("Connection") {
static states = States.make(() => [Connected, Disconnected]);
}
class Connected extends State.Atomic<Connected>()("Connected") {}
class Disconnected extends State.Atomic<Disconnected>()("Disconnected") {}
export const ConnectionChart = Statechart.make(Connection);
2 · Handle without a target
Connected accepts Heartbeat and stays Connected.
Bare on(Heartbeat) handles the event at Connected without selecting another state. Every send is Handled, the configuration remains unchanged, and the same local handler remains available.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export const Heartbeat = Schema.TaggedStruct("Heartbeat", {});
class Connection extends State.Compound<Connection>()("Connection") {
static states = States.make(() => [Connected, Disconnected]);
}
class Connected extends State.Atomic<Connected>()("Connected") {
static transitions = Transitions.make(this, ({ on }) => [on(Heartbeat)]);
}
class Disconnected extends State.Atomic<Disconnected>()("Disconnected") {}
export const ConnectionChart = Statechart.make(Connection);
Acceptance and movement are separate
The first frame defines Heartbeat, but neither state handles it. As before, event vocabulary is
not global behavior: only a handler owned by an active source can accept the event.
The second frame adds that handler to Connected without naming a destination:
on(Heartbeat);
This is a complete transition declaration. The missing target is intentional, not an omitted argument waiting to be inferred.
No target preserves the configuration
Send Heartbeat while Connected is active. The result is Handled, and the active path remains
Connection.Connected.
Send it again. The same handler is still available because the first event did not leave
Connected. The live readout counts actual Handled results while the topology stays fixed.
A targetless transition does not exit its source and reenter it. It selects no target at all. That distinction matters once entry, exit, and explicit reentry behavior are attached to states.
Targetless does not mean ignored
An unhandled event is Refused. A targetless handler accepts the event and produces Handled,
even when this minimal example attaches no other behavior.
Later, the Actions category will attach updates, emits, and raised events to targetless handlers. The transition then performs that authored work without changing which states are active. This page keeps those actions out so the no-target topology is visible on its own.
Ownership remains local
on(Heartbeat) belongs to Connected. It applies only while Connected is active; it does not
make Heartbeat a universal handler for every state in the chart.
If the same protocol input should be accepted across several children, place the handler on the nearest active ancestor that truly owns that rule instead of copying it into every leaf.
Do not fake it with a self-target
Naming the source state as a target still authors a targeted transition. It can participate in exit, entry, and reentry semantics that a targetless transition deliberately avoids.
Use bare on(Event) when the event should be handled in place. Use a target when the event should
select a state, even if that target is related to the current source.
Test the handler
- Send several heartbeats and confirm that every result is
HandledatConnected. - Move the handler to
Disconnectedand predict whether the initial configuration accepts it. - Place the handler on
Connectionand compare its lifetime with the child-owned version.