Guide

.reenter

Explicitly exit and rebuild a self or descendant transition source.

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

  1. Save a distinctive draft and open Profile in both frames.
  2. Remove .reenter() from the second frame and confirm its result matches the first.
  3. Add an entry action or mounted timer to Workspace and use it as a second restart witness.