Skip to content

anzorb/mobx-state-machine-router

Repository files navigation

MobX

MobX State Machine Router

Declarative, predictable routing for React & React Native powered by finite state machines and MobX

npm version npm downloads CI codecov license

Live DemoInstallationQuick StartAPIURL PersistenceExamples


Why?

Traditional routers map URLs to components. MobX State Machine Router takes a different approach:

  • 🎯 State Machine First — Define valid states and transitions. Invalid navigation is impossible by design.
  • ⚡ MobX Powered — Reactive state updates with fine-grained re-rendering. No unnecessary renders.
  • 🔗 URL Persistence Optional — Works great without URLs (React Native) or with hash/browser history.
  • 🔒 Type Safe — Full TypeScript support with inferred types for states, actions, and params.
  • 🪶 Lightweight — ~3KB gzipped with zero dependencies (MobX is a peer dependency).

Installation

# npm
npm install @mobx-state-machine-router/core mobx

# yarn
yarn add @mobx-state-machine-router/core mobx

# pnpm
pnpm add @mobx-state-machine-router/core mobx

Quick Start

import MobxStateMachineRouter, { TStates } from '@mobx-state-machine-router/core';

// 1. Define your states and actions as string literal types
type State = 'home' | 'products' | 'product-detail';
type Action = 'go-products' | 'view-product' | 'go-home';

type Params = {
  productId?: string;
};

// 2. Define the state machine
const states: TStates<State, Action> = {
  home: {
    actions: {
      'go-products': 'products',
    },
  },
  products: {
    actions: {
      'go-home': 'home',
      'view-product': 'product-detail',
    },
  },
  'product-detail': {
    actions: {
      'go-home': 'home',
      'go-products': 'products',
    },
  },
};

// 3. Create the router
const router = MobxStateMachineRouter<State, Params, Action>({
  states,
  currentState: { name: 'home', params: {} },
});

// 4. Navigate by emitting actions
router.emit('go-products');
console.log(router.currentState.name); // 'products'

// Pass params with navigation
router.emit('view-product', { productId: '123' });
console.log(router.currentState.params); // { productId: '123' }

Usage with React

import { observer } from 'mobx-react-lite';
import { router } from './router';

const App = observer(() => {
  const { name, params } = router.currentState;

  return (
    <div>
      <nav>
        <button onClick={() => router.emit('go-home')}>Home</button>
        <button onClick={() => router.emit('go-products')}>Products</button>
      </nav>

      {name === 'home' && <HomePage />}
      {name === 'products' && <ProductsPage />}
      {name === 'product-detail' && <ProductDetail id={params.productId} />}
    </div>
  );
});

API

MobxStateMachineRouter(options)

Creates a new router instance.

const router = MobxStateMachineRouter<States, Params, Actions>({
  states: TStates<States, Actions>,  // State machine definition
  currentState?: {                    // Initial state (optional)
    name: States,
    params: Params,
  },
  persistence?: IPersistence,         // URL persistence layer (optional)
});

router.currentState

Observable object containing the current state name and params.

router.currentState.name   // Current state name
router.currentState.params // Current params object

router.emit(action, params?)

Transition to a new state by emitting an action.

router.emit('go-home');                        // Simple navigation
router.emit('view-product', { id: '1' });      // With params

observeParam(router, property, paramName, callback)

Observe changes to a specific param.

import { observeParam } from '@mobx-state-machine-router/core';

observeParam(router, 'currentState', 'productId', (change) => {
  console.log('productId changed:', change.newValue);
});

Observing & Intercepting

Observe State Changes

import { observe, autorun } from 'mobx';

// React to any state change
observe(router, 'currentState', ({ newValue }) => {
  console.log('Navigated to:', newValue.name);
});

// Or use autorun for reactive effects
autorun(() => {
  analytics.track('page_view', { page: router.currentState.name });
});

Intercept State Changes

Guard navigation or redirect users:

import { intercept } from 'mobx';

intercept(router, 'currentState', (change) => {
  // Redirect unauthenticated users
  if (change.newValue.name === 'admin' && !isLoggedIn) {
    return { ...change, newValue: { name: 'login', params: {} } };
  }
  return change;
});

Async Interception

import interceptAsync from 'mobx-async-intercept';

interceptAsync(router, 'currentState', async (change) => {
  if (change.newValue.name === 'checkout') {
    const canCheckout = await validateCart();
    if (!canCheckout) {
      return { ...change, newValue: { name: 'cart-error', params: {} } };
    }
  }
  return change;
});

URL Persistence

Add URL synchronization with hash or browser history:

pnpm add @mobx-state-machine-router/url-persistence history
import MobxStateMachineRouter from '@mobx-state-machine-router/core';
import URLPersistence from '@mobx-state-machine-router/url-persistence';

// Add URL to each state
const states: TStates<State, Action> = {
  home: {
    actions: { 'go-products': 'products' },
    url: '/',
  },
  products: {
    actions: { 'view-product': 'product-detail' },
    url: '/products',
  },
  'product-detail': {
    actions: { 'go-products': 'products' },
    url: '/product',
  },
};

// Create router with URL persistence
const router = MobxStateMachineRouter<State, Params, Action>({
  states,
  currentState: { name: 'home', params: {} },
  persistence: URLPersistence(),  // Uses hash history by default
});

// Navigation now updates the URL!
router.emit('view-product', { productId: '123' });
// URL: /#/product?productId=123

Custom History

import { createBrowserHistory, createMemoryHistory } from 'history';

// Browser history (requires server configuration)
URLPersistence({ history: createBrowserHistory() });

// Memory history (useful for testing)
URLPersistence({ history: createMemoryHistory() });

Custom Serializers

Handle complex param types:

URLPersistence({
  serializers: {
    filters: {
      getter: (value) => JSON.parse(decodeURIComponent(value)),
      setter: (value) => encodeURIComponent(JSON.stringify(value)),
    },
  },
});

Examples

Live Demo — Try it in your browser!

Check out the example app for a complete demo with:

  • Multi-page navigation
  • URL persistence with hash routing
  • Query parameter filtering
  • TypeScript throughout

Run locally:

cd example
pnpm install
pnpm dev

TypeScript

This library is written in TypeScript and provides full type inference:

// States and actions are type-checked
router.emit('invalid-action'); // TS Error!

// Params are typed
router.emit('view-product', { productId: 123 }); // TS Error if wrong type

Packages

Package Description
@mobx-state-machine-router/core Core router with state machine logic
@mobx-state-machine-router/url-persistence URL synchronization with hash/browser history

Requirements

  • MobX 4.x, 5.x, or 6.x
  • Node.js 18+

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a PR.

# Install dependencies
pnpm install

# Run tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Build packages
pnpm build

# Run example app
cd example && pnpm dev

License

MIT © Anzor Bashkhaz

About

Reactive State Machine Based UIs for React and React Native + MobX

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors