Rxova
Skip to content

React Overview

@rxova/journey-react is a thin, typed React wrapper around @rxova/journey-core.

See the Core motivation: Core Motivation.

React bindings are a wrapper layer, not a second runtime.

createJourney(definition, options?) creates the core machine immediately in status: "idled" and returns a JourneyRuntime bundle:

  • machine
  • dispose()
  • useJourneySnapshot()
  • useJourneySelector(selector, equalityFn?)
  • useJourneyApi()
  • useStepApi(stepId)
  • useJourneyEvent(listener)
  • JourneyProvider
  • StepRenderer

Hooks work without a provider for reads and manual control. useJourneyApi() includes startJourney() for provider-free flows, and JourneyProvider supplies the views map, lifecycle callbacks, and client-side auto-start for an idled machine.

createJourneyBindings(...) and the older root-level React exports are not part of this package surface anymore. createJourney(...) is the supported runtime entrypoint.

createJourney(...) is stateful.

  • One call creates one machine instance immediately.
  • The returned hooks and components stay bound to that specific machine instance.
  • Reusing that returned runtime across multiple providers reuses the same journey state.
  • Creating a second independent journey means calling createJourney(...) again.

This is also what keeps the API strongly typed. Once the journey is created, the returned React API already knows the valid step ids, event names, and payload shapes.

The compatibility promises for that model are documented in the shared Stability Contract.

For the runtime architecture model, read Core Machine Architecture, especially the file-level pages in that section.

TypeScript stays first-class here too.

createJourney(...) captures journey types once, then the returned hooks and components stay typed without repeating generics at each callsite.

For deeper type modeling such as events, payload maps, and snapshots, see Core TypeScript.

  • createJourney(definition, options?)
  • hooks bound to the created machine
  • useStepApi(stepId) for step-scoped custom event typing
  • a JourneyProvider for views and lifecycle callbacks
  • a StepRenderer that renders the current step view

This keeps React ergonomic without moving runtime logic into components.

Create the runtime outside render when possible. If a component must own it, memoize it and either set disposeOnUnmount on JourneyProvider or call dispose() manually so you do not recreate journey state on every render.

If you need isolated state per request, per card, or per mounted route boundary, create a separate runtime in each owned boundary instead of reusing one module singleton.

import React from "react";
import { createJourney, type JourneyViews } from "@rxova/journey-react";
import type { JourneyDefinition } from "@rxova/journey-core";
type StepId = "details" | "payment" | "review";
type Context = { isVip: boolean };
type EventMap = { applyCoupon: unknown };
const definition: JourneyDefinition<Context, StepId, EventMap> = {
initial: "details",
context: { isVip: false },
steps: {
details: { meta: { title: "Details" } },
payment: { meta: { title: "Payment" } },
review: { meta: { title: "Review" } }
},
transitions: {
details: {
goToNextStep: [
{ to: "review", when: ({ context }) => context.isVip },
{ to: "payment", when: ({ context }) => !context.isVip }
]
},
payment: {
applyCoupon: [{ to: "review" }]
},
review: {
completeJourney: true
}
}
};
const checkoutJourney = createJourney(definition);
const Details = () => {
const api = checkoutJourney.useJourneyApi();
return <button onClick={() => void api.goToNextStep()}>Next</button>;
};
const Payment = () => {
const api = checkoutJourney.useJourneyApi();
return <button onClick={() => void api.send({ type: "applyCoupon" })}>Apply coupon</button>;
};
const Review = () => {
const api = checkoutJourney.useJourneyApi();
return <button onClick={() => void api.completeJourney()}>Finish</button>;
};
const views: JourneyViews<StepId> = {
details: Details,
payment: Payment,
review: Review
};
export const App = () => (
<checkoutJourney.JourneyProvider views={views}>
<checkoutJourney.StepRenderer />
</checkoutJourney.JourneyProvider>
);

Guard and updateContext failures resolve through result.error instead of rejecting, so fire-and-forget button handlers like void api.goToNextStep() do not surface as unhandled promise rejections.

React bindings do not redefine runtime behavior.

Core docs remain the source of truth for:

  • Core stays deterministic and framework-agnostic.
  • React stays focused on rendering and hook ergonomics.
  • Teams debug runtime behavior with Core mental models, then implement UI with React bindings.

Use React docs for how to wire Journey into React. Use Core docs for how Journey works under the hood.