Rxova
Skip to content

Linear

A linear journey uses declared step order as its default forward path.

import { createLinearJourney } from "@rxova/journey-core";
type StepId = "account" | "shipping" | "review";
type Context = { address: string };
type TerminationPayloads = {
complete: { orderId: string };
terminate: { reason: "cancelled" };
};
const machine = createLinearJourney<StepId, Context, TerminationPayloads>({
steps: [
"account",
{
id: "shipping",
metadata: { title: "Shipping" },
onLeave: async ({ snapshot }) => analytics.track("shipping_left", snapshot.context)
},
{
id: "review",
onEnter: ({ raise }) => {
// `raise` is a no-op for linear journeys.
}
}
] as const,
context: { address: "" }
});

The first declared step is the initial step; the startAt runtime option starts (and restarts) the journey directly at another declared step instead, with earlier steps neither entered nor visited. Duplicate ids and empty step arrays are rejected at creation time.

await waitUntilSettled(machine);
await machine.navigate.goToNextStep();
await machine.navigate.goToPreviousStep();
await machine.navigate.goToStepById("review");
await machine.navigate.goToStepByIndex(2);
await machine.navigate.goToLastVisitedStep();

Creating the machine already started it — autoStart defaults to true. The initial step is committed synchronously, so snapshot.currentStep is readable immediately, but asynchronous entry work is still in flight. waitUntilSettled is the small selector-based helper from the Quickstart; UI integrations can instead disable navigation while snapshot.transition.pending is true. Pass { autoStart: false } to hold the machine idle and call controls.start() yourself — the only way to observe the journey’s first stepEnter.

  • goToNextStep() follows the timeline forward when the pointer is behind its tip. At the tip, it falls back to the next step in declared order.
  • goToPreviousStep(n) moves the history pointer back and clamps to the first timeline entry.
  • goToStepById(id) may jump to any declared linear step and appends a new timeline entry.
  • goToStepByIndex(index) is the same jump addressed by declared-order index; an out-of-range or non-integer index rejects with "invalid-target".
  • goToLastVisitedStep() moves the pointer to the timeline tip.

Moving forward from an older history position uses the existing timeline. A new jump from an older position truncates the abandoned future before appending the destination.

goToStepById and goToStepByIndex are intentionally ungated escape hatches for occasional jumps in an otherwise ordered flow. They do not run next/previous work. If named jumps, guards, or branches become a routine part of the flow, convert the definition to graph mode so those transitions become explicit.

Reaching the last step does not complete the journey:

machine.controls.complete();

The optional third factory generic groups completion and termination payload types and narrows snapshot.machine.outcome. Omit it when terminal payload typing is not needed.

const snapshot = machine.getSnapshot();
snapshot.type; // "linear"
snapshot.steps.stepOrder;
snapshot.steps.totalSteps;
snapshot.steps.visitedStepCount;
snapshot.currentStep?.index;
snapshot.currentStep?.isFirstStep;
snapshot.currentStep?.isLastStep;

Metadata is available on the current step as snapshot.currentStep.metadata.

goToNextStep accepts work that must succeed before movement. The optional commit applies staged context updates atomically with the destination:

await machine.navigate.goToNextStep({
run: async ({ snapshot }) => submitShipping(snapshot.context),
commit: ({ result, updateContext }) => {
updateContext((context) => ({ ...context, shippingId: result.id }));
}
});

It is the only place a linear journey takes pre-move async. Backward navigation and goToStepById take none — update the context, then move:

machine.context.update((context) => ({ ...context, draftSaved: true }));
await machine.navigate.goToPreviousStep();

Step onLeave and onEnter are awaited post-commit effects. Their failures are reported but cannot roll navigation back.

When named events or guarded destinations become normal flow behaviour, rewrite the definition for createGraphJourney. The steps and context carry over unchanged; declared order becomes explicit transitions on each step’s on.