Rxova
Skip to content

Provider and Hooks API

React bindings are the UI-facing wrapper for the core machine.

Use this page for React integration details. Use Core docs for runtime semantics: Core API and Core Lifecycle.

The value returned from createJourney(...) is a JourneyRuntime.

One createJourney(...) call creates one machine instance immediately.

  • JourneyProvider does not create a machine. It only supplies views, lifecycle callbacks, and provider-owned startup for the already-created runtime.
  • Rendering the same runtime in multiple places shares one journey state.
  • Independent journeys require separate createJourney(...) calls.
  • createJourneyFactory(...) returns a typed helper for producing fresh runtimes from the same definition/options pair.

This is the tradeoff that keeps the React API fully typed without repeating generics at each hook callsite.

const checkout = createJourney(definition);
<checkout.JourneyProvider views={views}>
<checkout.StepRenderer />
</checkout.JourneyProvider>;
  • JourneyProvider supplies views and lifecycle callbacks.
  • JourneyProvider auto-starts an idled machine during the client commit phase, before child passive effects run.
  • JourneyProvider does not dispose the machine by default.
  • Provider-owned startup failures go to onError(error, { phase: "start" }) when that callback is provided.
  • Set disposeOnUnmount when the provider fully owns a component-scoped runtime and should clean it up on unmount.
  • StepRenderer renders the component matching snapshot.currentStepId.
  • StepRenderer must be used inside JourneyProvider.
  • checkout.useJourneySnapshot() Read-only runtime state for rendering.

  • checkout.useJourneyComputed() Read-only derived state for step progress and lifecycle flags.

  • checkout.useJourneySelector(selector, equalityFn?) Read only the selected part of snapshot state.

  • checkout.useJourneyEvent(listener) Subscribe to typed lifecycle and telemetry events.

  • checkout.useJourneyApi() Safe action surface for UI controls.

  • checkout.useStepApi(stepId) Same action surface as useJourneyApi(), but with send(...) narrowed to custom events handled by that step or global.

Hooks do not need a provider because they are closed over the created machine. Without a provider, startup is manual through checkout.useJourneyApi().startJourney() or checkout.machine.startJourney(). Server rendering still reads the initial idled snapshot. Provider-owned startup happens after hydration on the client. Use @rxova/journey-react for server-safe imports and @rxova/journey-react/client when a Next.js App Router client boundary should be explicit.

If a component owns the runtime, memoize it and opt into provider-owned disposal:

const makeCheckoutJourney = createJourneyFactory(definition);
const CheckoutCard = () => {
const checkout = React.useMemo(() => makeCheckoutJourney(), []);
return (
<checkout.JourneyProvider views={views} disposeOnUnmount>
<checkout.StepRenderer />
</checkout.JourneyProvider>
);
};

For request-scoped or route-scoped SSR usage, put the runtime inside the owned client boundary instead of exporting a module singleton:

"use client";
export function CheckoutFlow({ customerId }: { customerId: string }) {
const checkout = React.useMemo(
() =>
createJourney({
...definition,
context: {
...definition.context,
customerId
}
}),
[customerId]
);
return (
<checkout.JourneyProvider views={views} disposeOnUnmount>
<checkout.StepRenderer />
</checkout.JourneyProvider>
);
}

If you want two isolated journeys on one screen, create two runtimes:

const CheckoutCard = () => {
const checkout = React.useMemo(() => createJourney(definition), []);
return (
<checkout.JourneyProvider views={views}>
<checkout.StepRenderer />
</checkout.JourneyProvider>
);
};
export const DualCheckout = () => (
<>
<CheckoutCard />
<CheckoutCard />
</>
);

Use useJourneyComputed() when the UI needs derived progress flags without duplicating machine logic in components.

const computed = checkout.useJourneyComputed();
if (computed.mode === "linear") {
return (
<p>
Step {computed.activeStepIndex + 1} of {computed.stepCount}
</p>
);
}
return <p>Current step: {computed.activeStepId}</p>;

This hook is read-only. Keep commands in useJourneyApi().

Common methods:

  • startJourney()
  • goToNextStep
  • goToPreviousStep(steps?)
  • goToLastVisitedStep()
  • completeJourney
  • terminateJourney
  • send
  • updateContext
  • getStepMeta
  • clearStepError
  • resetJourney

Imperative jump:

await api.goToStepById("review");
await api.send({ type: "goToStepById", stepId: "review", payload: { source: "link" } });

Guard and updateContext failures resolve through result.error instead of rejecting.

updateContext follows core timing semantics. It updates the visible snapshot immediately, but it does not re-run an async transition already in evaluating-when. If the change must affect the current transition, apply it before send(...) or await the transition first. See Core Async Behavior.

Use useStepApi(stepId) inside step components when you want TypeScript to narrow send(...) to events that the current step can actually handle:

const EmailCode = () => {
const api = checkout.useStepApi("emailCode");
return <button onClick={() => void api.send({ type: "verifyCode" })}>Verify</button>;
};

The narrowed event set includes custom events declared on that step plus custom events declared in global. Built-in methods such as goToNextStep(), goToPreviousStep(), startJourney(), and resetJourney() stay available.

JourneyProvider accepts:

  • onError(error, { phase: "start" })

Use useJourneyEvent(...) or the underlying machine subscriptions for lifecycle observation.

Use the returned machine when you need low-level subscriptions or external integrations:

const checkout = createJourney(definition);
checkout.machine.subscribe(() => {
console.log(checkout.machine.getSnapshot());
});
checkout.machine.subscribeReset((event) => {
console.log("reset", event.stepId);
});
checkout.machine.startJourney();

dispose() is also returned as a convenience alias for machine.dispose(). JourneyProvider only disposes automatically when disposeOnUnmount is enabled, so shared or module-level runtimes can survive provider unmounts safely.

Even when called from React hooks, transition ordering, async phase handling, observability events, history behavior, and persistence are all defined by Core.

Reference pages: