Skip to content

utensils/xstate-react-router

Repository files navigation

xstate-react-router

npm version CI license

A fully type-safe router that drives React UI from XState state machines. Instead of URL → component, the state machine state → component.

Why?

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.

Install

npm install xstate-react-router

Peer dependencies

npm 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

Quick start

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>
  );
}

API

createMachineRouter<TMachine>()

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>();

<MachineRouter actor={actor}>

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.


<MachineRoute state="..." component={Comp} />

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.

<MachineRoute fallback component={Comp} />

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.

useMachineRouter()

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

Compound (nested) states

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>;

TypeScript

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} />

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors