Internal descendant movement preserves its compound source. When the domain needs a fresh owner
lifecycle instead, .reenter() makes that restart explicit.
Restart the owner when the domain calls for it.
A descendant target preserves its compound source by default. .reenter() keeps the target fixed while deliberately ending and rebuilding that source lifecycle.
1 · Preserve the source by default
The descendant target keeps Workspace active.
Workspace owns SelectProfile and targets Profile below itself. The internal default changes the active child while preserving Workspace and its saved draft.
import { State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ChangeDraft = Schema.TaggedStruct("ChangeDraft", {
draft: Schema.String,
});
const SelectProfile = Schema.TaggedStruct("SelectProfile", {});
export class Workspace extends State.Compound<Workspace>()("Workspace", {
draft: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(""))),
}) {
static states = States.make(() => [Overview, Profile]);
static get transitions() {
return Transitions.make(this, ({ on }) => [
on(SelectProfile, Profile),
]);
}
}
class Overview extends State.Atomic<Overview>()("Overview") {
static transitions = Transitions.make(this, ({ on }) => [
on(ChangeDraft).update(Workspace, ({ event }) => ({
draft: event.draft,
})),
]);
}
class Profile extends State.Atomic<Profile>()("Profile") {}
export const WorkspaceChart = Statechart.make(Workspace);
2 · Request a fresh lifecycle
.reenter() exits and rebuilds the source.
The source, event, and target stay the same. Adding .reenter() expands the transition domain to include Workspace, so its old draft ends and entry creates the empty default.
import { State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ChangeDraft = Schema.TaggedStruct("ChangeDraft", {
draft: Schema.String,
});
const SelectProfile = Schema.TaggedStruct("SelectProfile", {});
export class Workspace extends State.Compound<Workspace>()("Workspace", {
draft: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(""))),
}) {
static states = States.make(() => [Overview, Profile]);
static get transitions() {
return Transitions.make(this, ({ on }) => [
on(SelectProfile, Profile).reenter(),
]);
}
}
class Overview extends State.Atomic<Overview>()("Overview") {
static transitions = Transitions.make(this, ({ on }) => [
on(ChangeDraft).update(Workspace, ({ event }) => ({
draft: event.draft,
})),
]);
}
class Profile extends State.Atomic<Profile>()("Profile") {}
export const WorkspaceChart = Statechart.make(Workspace);
Hold the transition still
Both frames declare SelectProfile on Workspace and target the descendant Profile. The event,
source, and destination do not change. Only the transition domain changes.
That controlled comparison matters: .reenter() is not another way to name the target. It answers
a separate question about which active states must exit and enter while reaching it.
The default preserves the source
The first frame uses the internal default:
on(SelectProfile, Profile);
Save a draft and open the profile. Overview exits, Profile enters, and Workspace remains
active. Its state-owned draft therefore remains the same.
.reenter() expands the lifecycle boundary
The second frame changes one expression:
on(SelectProfile, Profile).reenter();
Now Workspace belongs to the transition's exit and entry domain. The old compound instance exits;
then a fresh Workspace enters with Profile active. The target is unchanged, but the route to it
crosses the owner's lifecycle boundary.
State data makes the restart visible
The draft facet is constructed with an empty default whenever Workspace enters. Save the same
distinctive draft in both frames before opening Profile.
The internal transition preserves it. The reentering transition replaces it with the empty default. This is not a manual reset action: it follows directly from ending one state incarnation and starting another.
Reentry restarts every mounted citizen
Facet reconstruction is only the smallest visible witness. Exiting Workspace also ends timers,
activities, children, and resource bindings mounted there. Entry arms or acquires fresh
incarnations according to the model.
Use .reenter() when that complete lifecycle restart is the intended domain behavior—for example,
to begin another bounded work epoch—not as a generic rendering refresh.
Reentry is local to the transition
Adding .reenter() changes this transition. It does not make Workspace globally reenter whenever
one of its descendants changes. Other transitions retain their own derived domains.
The modifier requires a self or descendant target because only those targets can preserve the source by default. It explicitly chooses the alternative for that otherwise-internal shape.
Test the lifecycle
- Save a distinctive draft and open
Profilein both frames. - Remove
.reenter()from the second frame and confirm its result matches the first. - Add an entry action or mounted timer to
Workspaceand use it as a second restart witness.