A fully type-safe router that drives React UI from XState state machines. Instead of URL → component, the state machine state → component.
Most React routers are URL-driven: the URL is the source of truth, and components render based on it. xstate-react-router flips that model — the state machine is the source of truth, and the router renders whichever component corresponds to the current machine state.
This is a natural fit when you use XState for application flow: auth flows, onboarding wizards, multi-step forms, or any app where the active "page" is determined by business logic rather than a URL.
npm install xstate-react-routernpm install xstate @xstate/react react react-dom| Package | Version |
|---|---|
xstate |
^5.0.0 |
@xstate/react |
^5.0.0 |
react |
^18.0.0 || ^19.0.0 |
react-dom |
^18.0.0 || ^19.0.0 |
import { createMachine } from 'xstate';
import { useActorRef } from '@xstate/react';
import { createMachineRouter } from 'xstate-react-router';
const appMachine = createMachine({
id: 'app',
initial: 'home',
states: {
home: { on: { GO_ABOUT: 'about', GO_SETTINGS: 'settings' } },
about: { on: { GO_HOME: 'home' } },
settings: { on: { GO_HOME: 'home' } },
},
});
// Create router components bound to your machine type
const { MachineRouter, MachineRoute, useMachineRouter } = createMachineRouter<typeof appMachine>();
function App() {
const actor = useActorRef(appMachine);
return (
<MachineRouter actor={actor}>
<MachineRoute state="home" component={HomePage} />
<MachineRoute state="about" component={AboutPage} />
<MachineRoute state="settings" component={SettingsPage} />
<MachineRoute fallback component={NotFoundPage} />
</MachineRouter>
);
}
function HomePage() {
const { send } = useMachineRouter();
return (
<div>
<h1>Home</h1>
<button onClick={() => send({ type: 'GO_ABOUT' })}>About</button>
<button onClick={() => send({ type: 'GO_SETTINGS' })}>Settings</button>
</div>
);
}Creates a set of router components and a hook bound to a specific XState machine type. Call this once per machine — typically in the same file where the machine is defined.
const { MachineRouter, MachineRoute, useMachineRouter } = createMachineRouter<typeof myMachine>();The provider component. Scans its <MachineRoute> children, finds the first one whose state matches snapshot.matches(), and renders only that component. Falls back to the fallback route if nothing matches, or renders null if no fallback is defined.
| Prop | Type | Required | Description |
|---|---|---|---|
actor |
ActorRefFrom<TMachine> |
Yes | A running XState actor (e.g. from useActorRef) |
children |
ReactNode |
Yes | <MachineRoute> elements |
Re-render behavior: MachineRouter re-renders only when the machine's state value changes, not on context-only updates. Context mutations that don't change the active state do not cause a route switch.
Declares a named route. Matched via snapshot.matches(state), which supports flat states, compound states, and parallel regions — anything XState's .matches() accepts.
| Prop | Type | Required | Description |
|---|---|---|---|
state |
StateValueFrom<TMachine> |
Yes | State value to match. Fully type-checked against the machine. |
component |
ComponentType |
Yes | Component to render when this route is active. |
Declares a fallback route rendered when no named route matches the current state. There should be at most one fallback per router. Order in JSX does not matter — a named match always takes priority.
| Prop | Type | Required | Description |
|---|---|---|---|
fallback |
true |
Yes | Marks this as the fallback route. |
component |
ComponentType |
Yes | Component to render when no named route matches. |
A hook for components rendered inside a <MachineRouter>. Provides full access to the actor, its snapshot, and the send function.
function SettingsPage() {
const { snapshot, send } = useMachineRouter();
return (
<div>
<p>Current state: {String(snapshot.value)}</p>
<button onClick={() => send({ type: 'GO_HOME' })}>Back</button>
</div>
);
}| Return value | Type | Description |
|---|---|---|
actor |
ActorRefFrom<TMachine> |
The running actor passed to <MachineRouter> |
snapshot |
SnapshotFrom<TMachine> |
Current machine snapshot — reactive, triggers re-renders |
send |
(event: EventFrom<TMachine>) => void |
Send a typed event to the actor |
Throws if called outside a <MachineRouter>.
Use an object to match compound state values. The state prop accepts anything snapshot.matches() accepts.
const stepMachine = createMachine({
id: 'steps',
initial: 'idle',
states: {
idle: { on: { START: 'active' } },
active: {
initial: 'step1',
states: {
step1: { on: { NEXT: 'step2' } },
step2: { on: { NEXT: 'step3' } },
step3: {},
},
},
},
});
const { MachineRouter, MachineRoute } = createMachineRouter<typeof stepMachine>();
<MachineRouter actor={actor}>
<MachineRoute state="idle" component={IdleView} />
<MachineRoute state={{ active: 'step1' }} component={Step1View} />
<MachineRoute state={{ active: 'step2' }} component={Step2View} />
<MachineRoute state={{ active: 'step3' }} component={Step3View} />
<MachineRoute fallback component={FallbackView} />
</MachineRouter>;createMachineRouter is fully generic. The state prop on <MachineRoute> is typed as StateValueFrom<TMachine> — TypeScript will surface invalid state names as type errors.
// ✅ Valid
<MachineRoute state="home" component={Home} />
// ✅ Valid compound state
<MachineRoute state={{ active: 'step1' }} component={Step1} />
// ❌ Type error — 'missing' is not a valid state
<MachineRoute state="missing" component={Home} />See CONTRIBUTING.md.
MIT — see LICENSE.