entry() runs when a state becomes active. exit() owns the other side of that lifecycle: an
ordered action list that runs whenever the state stops being active.
Run work when the state stops being active.
entry() owns arrival work. exit() gives the same state an ordered action list for every time its active lifecycle ends.
1 · Begin with arrivals
Gallery already counts each entry.
The familiar entry action increments galleryEntries whenever Gallery becomes active. Returning to Lobby exits Gallery, but no departure action records that lifecycle boundary yet.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const EnterGallery = Schema.TaggedStruct("EnterGallery", {});
const ReturnToLobby = Schema.TaggedStruct("ReturnToLobby", {});
const countGalleryEntry = Query.gen(function* () {
const museum = yield* Museum;
return { galleryEntries: museum.galleryEntries + 1 };
});
export class Museum extends State.Compound<Museum>()("Museum", {
galleryEntries: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
galleryExits: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
}) {
static states = States.make(() => [Lobby, Gallery]);
}
class Lobby extends State.Atomic<Lobby>()("Lobby") {
static transitions = Transitions.make(this, ({ on }) => [
on(EnterGallery, Gallery),
]);
}
class Gallery extends State.Atomic<Gallery>()("Gallery") {
static transitions = Transitions.make(this, ({ entry, on }) => [
entry().update(Museum, countGalleryEntry),
on(ReturnToLobby, Lobby),
]);
}
export const MuseumChart = Statechart.make(Museum);
2 · Give exit an action
Gallery counts each departure too.
Gallery now owns exit().update(...). Entering increments arrivals; returning increments departures. Repeat the cycle to see each action follow its own side of the state lifecycle.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const EnterGallery = Schema.TaggedStruct("EnterGallery", {});
const ReturnToLobby = Schema.TaggedStruct("ReturnToLobby", {});
const countGalleryEntry = Query.gen(function* () {
const museum = yield* Museum;
return { galleryEntries: museum.galleryEntries + 1 };
});
const countGalleryExit = Query.gen(function* () {
const museum = yield* Museum;
return { galleryExits: museum.galleryExits + 1 };
});
export class Museum extends State.Compound<Museum>()("Museum", {
galleryEntries: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
galleryExits: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
}) {
static states = States.make(() => [Lobby, Gallery]);
}
class Lobby extends State.Atomic<Lobby>()("Lobby") {
static transitions = Transitions.make(this, ({ on }) => [
on(EnterGallery, Gallery),
]);
}
class Gallery extends State.Atomic<Gallery>()("Gallery") {
static transitions = Transitions.make(this, ({ entry, exit, on }) => [
entry().update(Museum, countGalleryEntry),
exit().update(Museum, countGalleryExit),
on(ReturnToLobby, Lobby),
]);
}
export const MuseumChart = Statechart.make(Museum);
Begin with the arrival side
The first frame carries the Museum forward from the previous page. Entering Gallery increments
galleryEntries; returning to Lobby changes the active state but records no departure.
The model already has a galleryExits field so the second frame can change one behavior without
changing the visible data contract.
exit() belongs to the state that leaves
The second frame adds this branch to Gallery:
exit().update(Museum, countGalleryExit);
Enter the gallery and arrivals become 1 while departures remain 0. Return to the lobby and
departures become 1. A second cycle produces 2 and 2: each boundary action follows its own
side of each Gallery lifecycle.
Departure work is not tied to one route
ReturnToLobby is the only route out of Gallery in this small model, but the exit action does not
belong to that event edge. Add another transition out and the same state-owned departure work runs
for that route too.
This keeps cleanup, final bookkeeping, and other departure rules attached to the lifetime they describe instead of copying them across every possible outgoing transition.
The state still exists during its exit actions
Exit actions run as part of leaving the state, before its state-owned facet and mounted citizens are removed. An exit computation may therefore read the data whose lifecycle is ending and project any durable result to an owner that remains active.
This example updates Museum, which survives the child transition. Updating data owned only by
Gallery would be pointless here because that facet disappears when the exit completes.
Preserving the state means no exit
A targetless handler leaves the configuration unchanged, so it does not run exit actions. An internal descendant transition preserves its compound source, so that source does not exit either.
.reenter() makes the contrast explicit: it runs the source's exit actions, ends its mounted
lifecycle, enters it again, and then runs its entry actions.
A boundary branch is an action list
Like entry(), bare exit() is incomplete. Chain one or more actions or remove the branch. The
actions retain their authored order, and every exited state contributes its own list.
The later .update page will make the complete fold visible. For now, the two counters isolate the
ownership rule: arrivals belong to entry; departures belong to exit.
Test the departure
- Complete two enter/return cycles and confirm that arrivals lead departures only while inside.
- Add another event that leaves
Galleryand verify that it shares the same exit action. - Move the exit branch to
Museumand predict whether child-only navigation can trigger it.