Stately
State Machines

Routes

A route is a state node that can be directly navigated to by sending an xstate.route event. Routes are useful for mapping URL-based navigation (or any external navigation source) to state machine transitions, allowing you to jump directly to a specific state from anywhere in the machine.

Routes are syntactic sugar for transitions from the root state node to the target state. They follow all the same state machine rules — guards, entry/exit actions, and transition selection all work as expected.

Defining routes

Add a route property to any state node to make it routable. The state node must also have an id to be a valid route target:

import { setup, createActor } from 'xstate';

const machine = setup({}).createMachine({
  id: 'app',
  initial: 'home',
  states: {
    home: {
      id: 'home',
      route: {}
    },
    about: {
      id: 'about',
      route: {}
    },
    contact: {}  // No route — not directly navigable
  }
});

Send an xstate.route event with a to property referencing the target state's id (prefixed with #):

const actor = createActor(machine).start();

// Navigate to the 'about' state
actor.send({
  type: 'xstate.route',
  to: '#about'
});

actor.getSnapshot().value; // 'about'

Routing to the current state will re-enter it, executing exit and entry actions.

States without a route config will not respond to route events:

// 'contact' has no route, so this will not transition
actor.send({
  type: 'xstate.route',
  to: '#contact'
});

Route guards

You can add a guard to a route to conditionally allow navigation:

import { setup, createActor } from 'xstate';

const machine = setup({}).createMachine({
  id: 'app',
  initial: 'home',
  states: {
    home: {
      id: 'home',
      route: {}
    },
    dashboard: {
      id: 'dashboard',
      route: {
        guard: ({ context }) => context.role === 'editor'
      }
    }
  }
});

const actor = createActor(machine).start();

// Will NOT navigate — guard returns false
actor.send({ type: 'xstate.route', to: '#dashboard' });

If the guard returns false, the route event is ignored and the state does not change.

Routes require an id. A state node with route: {} but no id is not routable — the id is required to generate a valid route target.

Deeply nested routes

Routes work with nested state machines. You can navigate directly to a deeply nested state from anywhere:

import { setup, createActor } from 'xstate';

const machine = setup({}).createMachine({
  id: 'app',
  initial: 'home',
  states: {
    home: {
      id: 'home',
      route: {}
    },
    dashboard: {
      initial: 'overview',
      states: {
        overview: {
          id: 'overview',
          route: {}
        },
        settings: {
          id: 'settings',
          route: {}
        }
      }
    }
  }
});

const actor = createActor(machine).start();

// Jump directly to a nested state
actor.send({ type: 'xstate.route', to: '#overview' });

actor.getSnapshot().value;
// { dashboard: 'overview' }

Route targets use the state's id (e.g. #overview), not dot-separated paths. #dashboard.overview is not a valid route target.

Routes in parallel states

Routes work naturally with parallel states. Routing to a state in one region does not affect the other regions:

import { setup, createActor } from 'xstate';

const todoMachine = setup({}).createMachine({
  id: 'todos',
  type: 'parallel',
  states: {
    todo: {
      initial: 'new',
      states: {
        new: {},
        editing: {}
      }
    },
    filter: {
      initial: 'all',
      states: {
        all: {
          id: 'filter-all',
          route: {}
        },
        active: {
          id: 'filter-active',
          route: {}
        },
        completed: {
          id: 'filter-completed',
          route: {}
        }
      }
    }
  }
});

const actor = createActor(todoMachine).start();

actor.send({ type: 'xstate.route', to: '#filter-active' });

actor.getSnapshot().value;
// { todo: 'new', filter: 'active' }

TypeScript

Route events are strongly typed. Only states with both a route config and an id will be valid to targets:

import { setup, createActor } from 'xstate';

const machine = setup({
  types: {
    events: {} as
      | { type: 'navigate'; page: string }
  }
}).createMachine({
  id: 'app',
  initial: 'home',
  states: {
    home: {
      id: 'home',
      route: {}
    },
    about: {
      // No route — not a valid target
    }
  }
});

const actor = createActor(machine).start();

// Valid
actor.send({ type: 'xstate.route', to: '#home' });

// @ts-expect-error — 'about' has no route config
actor.send({ type: 'xstate.route', to: '#about' });

On this page