@rxova/journey-react
v1.0.0-rc.2
Published
React bindings for journey.
Readme
@rxova/journey-react
Typed React bindings for multi-step UI flows.
@rxova/journey-react is approaching a 1.0.0-rc contract freeze. The key runtime rule is unchanged:
one createJourney(...) call creates one machine instance immediately, and the returned hooks/components stay bound to that instance.
Install
npm i @rxova/journey-react @rxova/journey-coreUse the root entry for server-safe imports. When a Next.js App Router client boundary should be explicit,
import from @rxova/journey-react/client.
Quickstart
import { createJourney, type JourneyViews } from "@rxova/journey-react";
import type { JourneyDefinition } from "@rxova/journey-core";
type StepId = "start" | "review";
type Context = { name: string };
const definition: JourneyDefinition<Context, StepId> = {
initial: "start",
context: { name: "" },
steps: { start: {}, review: {} },
transitions: {
start: { goToNextStep: [{ to: "review" }] },
review: { completeJourney: true }
}
};
const signup = createJourney(definition);
const Start = () => {
const api = signup.useJourneyApi();
const snap = signup.useJourneySnapshot();
return (
<div>
<p>Hello, {snap.context.name || "stranger"}</p>
<button onClick={() => void api.goToNextStep()}>Next</button>
</div>
);
};
const Review = () => {
const api = signup.useJourneyApi();
return <button onClick={() => void api.completeJourney()}>Submit</button>;
};
const views: JourneyViews<StepId> = { start: Start, review: Review };
export const App = () => (
<signup.JourneyProvider views={views}>
<signup.StepRenderer />
</signup.JourneyProvider>
);Hooks
createJourney() returns a runtime with bound hooks:
useJourneySnapshot()— full snapshot:currentStepId,context,history,status,asyncuseJourneyApi()— runtime commands:startJourney,goToNextStep,goToPreviousStep,completeJourney,send, etc.useStepApi(stepId)— step-scoped command surface withsend(...)narrowed to custom events handled by that step orglobaluseJourneyComputed()— derived state:mode,activeStepId,isLoading,isFirstStep,isLastStepuseJourneySelector(selector, eq?)— subscribe to a slice of the snapshotuseJourneyEvent(listener)— stream lifecycle events for analytics
Navigation
const api = signup.useJourneyApi();
await api.startJourney();
await api.goToNextStep();
await api.goToPreviousStep();
await api.goToLastVisitedStep();
await api.completeJourney();
await api.terminateJourney();
await api.goToStepById("review");
api.updateContext((ctx) => ({ ...ctx, name: "Ada" }));
api.resetJourney();Transition failures resolve through result.error instead of rejecting, so void api.goToNextStep() is safe from unhandled promise rejections.
For step components, useStepApi(stepId) returns the same commands but narrows send(...) to custom events handled by that step or by global transitions:
const api = signup.useStepApi("start");
void api.send({ type: "submit" });Custom Step Renderer
StepRenderer is a convenience — it just looks up the current step's view from the views record and renders it. You can build your own if you need transitions, animations, or a different rendering strategy:
const MyStepRenderer = () => {
const { currentStepId } = signup.useJourneySnapshot();
const View = views[currentStepId];
if (!View) return <p>Unknown step</p>;
return <View />;
};Plugins
import { createPersistencePlugin } from "@rxova/journey-core/persistence";
const signup = createJourney(definition, {
plugins: [createPersistencePlugin({ key: "signup", version: 1 })],
defaultTimeoutMs: 30_000
});Runtime Ownership
Each createJourney() call creates one machine instance. The returned hooks are permanently bound to it.
- Rendering multiple providers from the same runtime shares one journey state
JourneyProviderauto-starts anidledruntime, but does not dispose it by default- Provider-free flows can start manually through
useJourneyApi().startJourney()ormachine.startJourney() - Provider-owned startup failures are reported through
onError(error, { phase: "start" }) - Set
disposeOnUnmountwhen a provider fully owns a component-scoped runtime - Independent instances require separate
createJourney()calls createJourneyFactory()returns a typed helper for producing fresh runtimes from the same definition/options pair and is the preferred path when request-scoped or boundary-scoped isolation mattersdispose()tears down subscriptions when the runtime is no longer needed
Documentation
License
MIT
