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.
Ownership Model
One createJourney(...) call creates one machine instance immediately.
JourneyProviderdoes not create a machine. It only suppliesviews, 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.
JourneyProvider And StepRenderer
const checkout = createJourney(definition);
<checkout.JourneyProvider views={views}>
<checkout.StepRenderer />
</checkout.JourneyProvider>;
JourneyProvidersuppliesviewsand lifecycle callbacks.JourneyProviderauto-starts anidledmachine during the client commit phase, before child passive effects run.JourneyProviderdoes not dispose the machine by default.- Provider-owned startup failures go to
onError(error, { phase: "start" })when that callback is provided. - Set
disposeOnUnmountwhen the provider fully owns a component-scoped runtime and should clean it up on unmount. StepRendererrenders the component matchingsnapshot.currentStepId.StepRenderermust be used insideJourneyProvider.
Hooks And Responsibilities
-
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.
Hooks do not need a provider because they are closed over the created machine. Without a provider, startup is manual through checkout.useJourneyApi().start() or checkout.machine.start().
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 />
</>
);
Module-level singletons are safe to show and hide with JourneyProvider because the provider does not dispose by default.
// ✅ Safe: hiding the provider does not dispose the shared runtime
const journey = createJourney(definition);
function App() {
const [show, setShow] = React.useState(true);
return show ? <journey.JourneyProvider views={views}>...</journey.JourneyProvider> : null;
}
If the runtime is component-owned, opt into provider disposal so each mount gets a fresh machine:
// ✅ Each mount creates a fresh machine and disposes it on unmount
function App() {
const [show, setShow] = React.useState(true);
const journey = React.useMemo(() => createJourney(definition), []);
return show ? (
<journey.JourneyProvider views={views} disposeOnUnmount>
...
</journey.JourneyProvider>
) : null;
}
useJourneyComputed()
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().
useJourneyApi() Surface
Common methods:
start()goToNextStepgoToPreviousStep(steps?)goToLastVisitedStep()completeJourneyterminateJourneysendupdateContextgetStepMetaclearStepErrorresetJourney
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.
Provider Callbacks
JourneyProvider accepts:
onStart(event)onComplete(event)onTerminate(event)onError(error, { phase: "start" })
They are thin wrappers over the machine lifecycle subscriptions.
onStartfires when the provider transitions anidledmachine torunningon the client, including restarts afterresetJourney().onCompleteandonTerminateonly fire for emitted terminal lifecycle events.
Direct Machine Access
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.start();
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.
Important Boundary
Even when called from React hooks, transition ordering, async phase handling, observability events, history behavior, and persistence are all defined by Core.
Reference pages: