Rxova
Skip to content

Core concepts

A definition describes steps, initial context, hooks, and, for graph journeys, transitions. A factory validates and normalizes that definition, then creates a live machine.

const machine = createLinearJourney({
steps: ["profile", "confirm"] as const,
context: { name: "" }
});

The machine’s methods remain stable for its lifetime. Runtime values are read from snapshots.

Factories also accept runtime options: autoStart (defaults to true; pass false to hold the machine idle and start it yourself), startAt (start directly at a given step; overrides graph initial), persist (persistence-plugin sugar that also restores a saved position), defaultTimeoutMs, onListenerError (routes isolated subscriber failures), and plugins.

Every step has an id. A full step config can add static metadata, onLeave, and onEnter.

{
id: "profile",
metadata: { title: "Your profile" },
onLeave: ({ snapshot }) => analytics.track("profile_left", snapshot.context)
}

Metadata is definition data. The current step exposes it at snapshot.currentStep.metadata.

Context is application data that changes during a run. Replace it immutably through the machine or the hook-local updater:

machine.context.update((context) => ({ ...context, name: "Ada" }));
// Inside onLeave, onEnter, or onTransition:
updateContext((context) => ({ ...context, submitted: true }));

Hook updates apply immediately after navigation has committed. Navigation-work updates are staged separately and publish atomically with the move.

Graph journeys declare events as a discriminated union and transitions as an event-keyed map.

type Event =
| { type: "SAVE"; payload: { draftId: string } }
| { type: "CANCEL" };
steps: {
edit: { on: { SAVE: "review", CANCEL: "done" } },
review: {},
done: {}
}

Send an event with machine.send("SAVE", { draftId: "d1" }). Linear machines do not have send.

A graph transition’s when guard is a synchronous, pure predicate over context and injected handlers. Guards are evaluated both during sends and during graph snapshot derivation.

when: ({ context, handlers }) => context.accepted && handlers.isAllowed();

For caller-driven next/previous movement, pass asynchronous validation as navigation work.

The runtime has one transactional work point and three effect points:

Work/effectTimingCan block?Available on
Navigation run/commitBefore commitYesNext and previous
Step onLeaveAfter commitNoLinear and graph
Transition onTransitionAfter onLeaveNoGraph
Step onEnterAfter onTransitionNoLinear and graph

Hook arguments include snapshot, from, to, event, updateContext, and raise. event is null for linear and timeline moves. raise queues graph events after the current move settles.

A snapshot is an immutable, internally consistent read model:

const snapshot = machine.getSnapshot();
snapshot.status;
snapshot.context;
snapshot.currentStep;
snapshot.transition;
snapshot.history;
snapshot.machine;
snapshot.plugins;

snapshot.type is either "linear" or "graph" and narrows shape-specific fields.

History is a browser-like timeline plus a pointer:

  • timeline contains the realized path;
  • currentIndex points at the active timeline entry;
  • visited records whether each step has ever been entered in this run;
  • canGoBack and canGoForward are derived from the pointer.

Moving back does not erase the future. Appending a new destination while behind the tip creates a new branch and drops the abandoned future.

StatusMeaning
idleCreated but not started.
runningNavigation and graph sends are accepted.
pausedState is retained, but navigation is rejected.
completedExplicitly completed.
terminatedExplicitly terminated.

Completion and termination set snapshot.machine.outcome. Only restart() starts a fresh run from a terminal status.

Linear factories can declare terminal payload types without adding runtime configuration:

type TerminationPayloads = {
complete: { receiptId: string };
terminate: { reason: "cancelled" };
};
const machine = createLinearJourney<"form" | "result", {}, TerminationPayloads>({
steps: ["form", "result"],
context: {}
});

The third generic constrains payloads passed to controls.complete and controls.terminate and narrows the corresponding snapshot.machine.outcome union.

Plugins observe the runtime and contribute namespaced extensions:

machine.plugins.replay.getReplaySession();
machine.getSnapshot().plugins.replay;

They cannot intercept or rewrite navigation in V1.