JSPM

xstate

2.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1884070
  • Score
    100M100P100Q188778F
  • License MIT

Simple JavaScript Finite State Machines and Statecharts

Package Exports

  • xstate
  • xstate/lib/graph
  • xstate/lib/types
  • xstate/lib/utils

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (xstate) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

XState

Simple, stateless JavaScript finite state machines and statecharts.

Why?

Read the slides (video) or check out these resources for learning about the importance of finite state machines and statecharts in user interfaces:

Visualizing state machines and statecharts

The JSON-based notation used here to declaratively represent finite state machines and statecharts can be copy-pasted here: https://codepen.io/davidkpiano/pen/ayWKJO/ which will generate interactive state transition diagrams.

Getting Started

  1. npm install xstate --save
  2. import { Machine } from 'xstate';

Finite State Machines

Light Machine
import { Machine } from 'xstate';

const lightMachine = Machine({
  key: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow',
      }
    },
    yellow: {
      on: {
        TIMER: 'red',
      }
    },
    red: {
      on: {
        TIMER: 'green',
      }
    }
  }
});

const currentState = 'green';

const nextState = lightMachine
  .transition(currentState, 'TIMER')
  .value;

// => 'yellow'

Hierarchical (Nested) State Machines

Hierarchical Light Machine
import { Machine } from 'xstate';

const pedestrianStates = {
  initial: 'walk',
  states: {
    walk: {
      on: {
        PED_TIMER: 'wait'
      }
    },
    wait: {
      on: {
        PED_TIMER: 'stop'
      }
    },
    stop: {}
  }
};

const lightMachine = Machine({
  key: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      },
      ...pedestrianStates
    }
  }
});

const currentState = 'yellow';

const nextState = lightMachine
  .transition(currentState, 'TIMER')
  .toString(); // toString() only works for non-parallel machines

// => 'red.walk' 

lightMachine
  .transition('red.walk', 'PED_TIMER')
  .toString();

// => 'red.wait'

Object notation for hierarchical states:

// ...
const waitState = lightMachine
  .transition('red.walk', 'PED_TIMER')
  .value;

// => { red: 'wait' }

lightMachine
  .transition(waitState, 'PED_TIMER')
  .value;

// => { red: 'stop' }

lightMachine
  .transition('red.stop', 'TIMER')
  .value;

// => 'green'

Parallel States

const wordMachine = Machine({
  parallel: true,
  states: {
    bold: {
      initial: 'off',
      states: {
        on: {
          on: { TOGGLE_BOLD: 'off' }
        },
        off: {
          on: { TOGGLE_BOLD: 'on' }
        }
      }
    },
    underline: {
      initial: 'off',
      states: {
        on: {
          on: { TOGGLE_UNDERLINE: 'off' }
        },
        off: {
          on: { TOGGLE_UNDERLINE: 'on' }
        }
      }
    },
    italics: {
      initial: 'off',
      states: {
        on: {
          on: { TOGGLE_ITALICS: 'off' }
        },
        off: {
          on: { TOGGLE_ITALICS: 'on' }
        }
      }
    },
    list: {
      initial: 'none',
      states: {
        none: {
          on: { BULLETS: 'bullets', NUMBERS: 'numbers' }
        },
        bullets: {
          on: { NONE: 'none', NUMBERS: 'numbers' }
        },
        numbers: {
          on: { BULLETS: 'bullets', NONE: 'none' }
        }
      }
    }
  }
});

const boldState = wordMachine
  .transition('bold.off', 'TOGGLE_BOLD')
  .value;

// {
//   bold: 'on',
//   italics: 'off',
//   underline: 'off',
//   list: 'none'
// }

const nextState = wordMachine
  .transition({
    bold: 'off',
    italics: 'off',
    underline: 'on',
    list: 'bullets'
  }, 'TOGGLE_ITALICS')
  .value;

// {
//   bold: 'off',
//   italics: 'on',
//   underline: 'on',
//   list: 'bullets'
// }

History States

To provide full flexibility, history states are more arbitrarily defined than the original statechart specification. To go to a history state, use the special key $history.

Payment Machine
const paymentMachine = Machine({
  initial: 'method',
  states: {
    method: {
      initial: 'cash',
      states: {
        cash: { on: { SWITCH_CHECK: 'check' } },
        check: { on: { SWITCH_CASH: 'cash' } }
      },
      on: { NEXT: 'review' }
    },
    review: {
      on: { PREVIOUS: 'method.$history' }
    }
  }
});

const checkState = paymentMachine
  .transition('method.cash', 'SWITCH_CHECK');

// => State {
//   value: { method: 'check' },
//   history: { $current: { method: 'cash' }, ... }
// }

const reviewState = paymentMachine
  .transition(checkState, 'NEXT');

// => State {
//   value: 'review',
//   history: { $current: { method: 'check' }, ... }
// }

const previousState = paymentMachine
  .transition(reviewState, 'PREVIOUS')
  .value;

// => { method: 'check' }

More code examples coming soon!

Examples

import React, { Component } from 'react'
import { Machine } from 'xstate'

const ROOT_URL = `https://api.github.com/users`
const myMachine = Machine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        CLICK: 'loading'
      }
    },
    loading: {
      on: {
        RESOLVE: 'data',
        REJECT: 'error'
      }
    },
    data: {
      on: {
        CLICK: 'loading'
      }
    },
    error: {
      on: {
        CLICK: 'loading'
      }
    }
  }
})

class App extends Component {
  state = {
    data: {},
    dataState: 'idle',
    input: ''
  }

  searchRepositories = async () => {
    try {
      const data = await fetch(`${ROOT_URL}/${this.state.input}`).then(response => response.json())
      this.setState(({ data }), this.transition('RESOLVE'))

    } catch (error) {
      this.transition('REJECT')
    }
  }

  commands = {
    loading: this.searchRepositories
  }
  transition = action => {
    const { dataState } = this.state

    const newState = myMachine.transition(dataState, action).value
    const command = this.commands[newState]

    this.setState(
      {
        dataState: newState
      },
      command
    )
  }

  render() {
    const { data, dataState } = this.state
    const buttonText = {
      idle: 'Fetch Github',
      loading: 'Loading...',
      error: 'Github fail. Retry?',
      data: 'Fetch Again?'
    }[dataState]
    return (
      <div>
        <input
          type="text"
          value={this.state.input}
          onChange={e => this.setState({ input: e.target.value })}
        />
        <button
          onClick={() => this.transition('CLICK')}
          disabled={dataState === 'loading'}
        >
          {buttonText}
        </button>
        {data && <div>{JSON.stringify(data, null, 2)}</div>}
        {dataState === 'error' && <h1>Error!!!</h1>}
      </div>
    )
  }
}

export default App