Skip to main content
React 1.0.0-rc.1

React Overview

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

Motivation

See the Core motivation: Core Motivation.

Architecture

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()
  • useJourneyEvent(listener)
  • JourneyProvider
  • StepRenderer

Hooks work without a provider for reads and manual control. useJourneyApi() includes start() 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.

Runtime Ownership

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 In React

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.

What The React Package Gives You

  • createJourney(definition, options?)
  • hooks bound to the created machine
  • 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.

React Example

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.

What Still Lives In Core

React bindings do not redefine runtime behavior.

Core docs remain the source of truth for:

Why This Split Is Useful

  • 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.

One-Line Mental Model

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