Statecharts make topology explicit, but real systems repeat useful behavior: playback controls, approval flows, connection lifecycles, retrying jobs, and protocol sessions. A Component makes one state tree reusable without turning it into an opaque widget or a running singleton.
Reuse authored structure, not instances
Component.make gives a reusable state tree an explicit authoring boundary. Define the root and
its private State vocabulary inside the callback, then return the root. Motive captures the States
reachable from that root, their transitions, and the event alphabet those transitions use. It does
not start an instance, allocate runtime state, or register global behavior.
PlaybackReusable authored states, events, and transitions. No location and no running identity.
MediaApp.PlayerOne concrete authored occurrence with qualified descendants and host extensions.
The captured classes remain ordinary statechart structure. Paused still owns its Toggle
transition; Component does not add a second dispatch system around the model.
A placement gives the definition a location
const Playback = Component.make(() => {
class Playback extends State.Compound<Playback>()("Playback") {
static states = States.make(() => [Paused, Playing]);
}
class Paused extends State.Atomic<Paused>()("Paused") {}
class Playing extends State.Atomic<Playing>()("Playing") {}
return Playback;
});
class Player extends Playback.as<Player>()("Player") {}
The shared name is deliberate. Inside the callback, Playback names the root State; outside it,
Playback names the reusable Component whose topology begins at that root.
The definition says what can be placed. Player says where this occurrence lives. When MediaApp
includes the placement, its topology contains Player.Paused and Player.Playing; the template
name Playback does not become a hidden extra path segment.
The Self generic preserves the placement's exact static identity. Targets such as
Player.Paused are checked state classes, not strings reconstructed from naming conventions.
The open form is valid when the root State is intentionally part of the surrounding module's vocabulary:
class Playback extends State.Compound<Playback>()("Playback") {
// ...
}
const PlaybackComponent = Component.make(() => Playback);
Use that form when another authoring surface genuinely needs the unplaced Playback symbol. When
the classes exist only to define the reusable Component, enclosing them makes that ownership
visible in the source.
The host owns the extension surface
A placement is a real state class, so its host may add behavior at that boundary:
class Player extends Playback.as<Player>()("Player") {
static transitions = Transitions.make(this, ({ on }) => [
on(Reset, Player.Paused),
]);
}
Toggle remains definition-owned. Reset is placement-owned. The compiled chart merges both
surfaces into one transition topology without copying or mutating Playback.
That merge is checked for semantic conflicts. A host cannot silently shadow an unguarded definition arm for the same event; any deliberate guarded takeover must remain explicit in the authored structure.
Follow the ordinary tree into one placed and extended occurrence:
Reusable topology
Definitions become real at a placement.
Watch one ordinary state tree become a reusable definition, then a concrete placement with its own host-owned behavior.
1 · Begin with an ordinary state tree
Playback already owns one complete behavior.
Paused and Playing form one ordinary compound state with a Toggle event. Nothing about the statechart semantics changes when this same tree later becomes reusable; Component begins by preserving the structure we already understand.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Toggle extends Schema.TaggedClass<Toggle>()("Toggle", {}) {}
export class Playback extends State.Compound<Playback>()("Playback") {
static states = States.make(() => [Paused, Playing]);
}
class Paused extends State.Atomic<Paused>()("Paused") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Playing),
]);
}
class Playing extends State.Atomic<Playing>()("Playing") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Paused),
]);
}
export const PlaybackChart = Statechart.make(Playback);
2 · Capture, then place
The definition is reusable; Player is the authored location.
Component.make captures Playback and its event alphabet without mounting it. Playback.as<Player>()("Player") creates one concrete placement whose descendants become Player.Paused and Player.Playing inside MediaApp.
import { Component, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Toggle extends Schema.TaggedClass<Toggle>()("Toggle", {}) {}
const Playback = Component.make(() => {
class Playback extends State.Compound<Playback>()("Playback") {
static states = States.make(() => [Paused, Playing]);
}
class Paused extends State.Atomic<Paused>()("Paused") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Playing),
]);
}
class Playing extends State.Atomic<Playing>()("Playing") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Paused),
]);
}
return Playback;
});
export class MediaApp extends State.Compound<MediaApp>()("MediaApp") {
static states = States.make(() => [Player]);
}
class Player extends Playback.as<Player>()("Player") {}
export const MediaAppChart = Statechart.make(MediaApp);
3 · Extend at the host boundary
A placement may add host behavior without editing the template.
Player adds one Reset arm that targets its placement-qualified Paused node. Toggle still belongs to the captured definition, while Reset belongs to this host placement; the two surfaces merge without copying or mutating Playback.
import { Component, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Toggle extends Schema.TaggedClass<Toggle>()("Toggle", {}) {}
export class Reset extends Schema.TaggedClass<Reset>()("Reset", {}) {}
export const Playback = Component.make(() => {
class Playback extends State.Compound<Playback>()("Playback") {
static states = States.make(() => [Paused, Playing]);
}
class Paused extends State.Atomic<Paused>()("Paused") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Playing),
]);
}
class Playing extends State.Atomic<Playing>()("Playing") {
static transitions = Transitions.make(this, ({ on }) => [
on(Toggle, Paused),
]);
}
return Playback;
});
export class MediaApp extends State.Compound<MediaApp>()("MediaApp") {
static states = States.make(() => [Player]);
}
class Player extends Playback.as<Player>()("Player") {
static transitions = Transitions.make(this, ({ on }) => [
on(Reset, Player.Paused),
]);
}
export const MediaAppChart = Statechart.make(MediaApp);
The behavior survives placement unchanged
The first frame establishes the baseline: Toggle cycles the ordinary Playback chart between
Paused and Playing. Capturing the tree in the second frame changes its location, not its
behavior. The same event now moves through Player.Paused and Player.Playing.
This is the key promise of Component reuse: topology stays visible and statechart semantics remain the only computational model.
Placement-owned behavior stays local
The final frame adds Reset only to Player. Toggle into Player.Playing, then reset to the
placement-qualified Player.Paused. Another placement of the same Component could expose a
different host extension without changing this one or the captured definition.
The placement tag is therefore part of model identity, not presentation copy. It appears in the compiled graph, qualifies state paths and occurrence references, and contributes to chart hashing.
Test the definition boundary
- Compile the original state tree and the placed tree, then verify their definition-owned event behavior is equivalent under the qualified path.
- Verify the Component definition allocates no instance or runtime service by itself.
- Add a placement-owned event and verify it targets the placement-qualified child.
- Place the same definition under two different tags and verify their paths and occurrences stay distinct.
- Attempt to shadow an unguarded definition arm and verify authoring rejects the conflict.
- Inspect the compiled topology and verify no hidden template path segment appears.
Next, Repeated Placements and Occurrence Identity places the same definition more than once and follows the identity each occurrence receives.