Guide

entry

Run an ordered action list whenever its owning state becomes active.

Transitions decide which states become active. entry() attaches ordered work to the lifecycle of one state, so that work runs whenever the state becomes active—regardless of which route entered it.

Run work when the state becomes active.

Transitions choose the active geometry. entry() gives a state an ordered action list that runs every time that state becomes active.

1 · First, enter the state

A transition makes Gallery active.

EnterGallery moves the museum from Lobby to Gallery, and ReturnToLobby moves it back. The geometry changes, but Gallery does not yet declare any work for its own entry.

import { 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", {});

export class Museum extends State.Compound<Museum>()("Museum", {
  galleryEntries: 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, ({ on }) => [
    on(ReturnToLobby, Lobby),
  ]);
}

export const MuseumChart = Statechart.make(Museum);

2 · Give entry an action

Gallery counts every time it becomes active.

Gallery now owns entry().update(...). Entering it increments the root count; leaving does not. Return and enter again to run the same state-owned action for a new 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 };
});

export class Museum extends State.Compound<Museum>()("Museum", {
  galleryEntries: 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);

Reaching a state and entering it are different facts

The first frame moves between Lobby and Gallery. EnterGallery selects Gallery; the chart exits the old child and enters the new one. Nothing else needs to happen merely because the target was reached.

That transition is one route into Gallery. Entry behavior belongs to the state instead when every route into it should perform the same work.

entry() belongs to the state

The second frame adds one boundary branch to Gallery:

entry().update(Museum, countGalleryEntry);

Enter the gallery and watch galleryEntries become 1. Return to the lobby; the value stays put. Enter again and it becomes 2. The action follows each new Gallery lifecycle rather than a particular event name.

Entry is not another transition

An entry branch has no event or target. It runs because its owning state appears in the transition's entry set. Moving among descendants while that state remains active does not run it again.

Conversely, entering through another event, an eventless transition, completion, history, or a parent transition still runs the same entry actions when that route activates the state.

Initial states enter too

Starting a chart constructs its initial active path. Every state on that path is entered, so its entry actions run before the initial snapshot is published.

Lobby has no entry action in this example. If the counter belonged to Lobby.entry(), it would already be 1 when the client finishes start().

Reentry creates another entry

The .reenter() modifier from the previous page deliberately exits and re-enters its source. Its entry actions therefore run again. A targetless handler does not enter a state and cannot trigger entry behavior merely by being handled in place.

This makes lifecycle intent visible in the model: preserve the state to preserve its mounted work, or reenter it when a fresh lifecycle—including fresh entry actions—is required.

A boundary branch is an action list

Bare entry() is incomplete. Chain at least one action such as .update, .raise, or .emit, or remove the branch. Multiple chained actions retain their authored order.

This page uses .update only as a visible witness for entry. The Actions pages ahead treat updates directly: what they may read, which state owns the changed data, and how several updates fold.

Test the lifecycle

  1. Enter, return, and enter again; confirm that only entries into Gallery increment the count.
  2. Move the boundary branch to Lobby and predict the value immediately after start().
  3. Add a second transition into Gallery and confirm that the state-owned action covers both routes.