akonwi / xstate

Stateless JS Finite State Machines and Statecharts

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

About

Stateless JS Finite State Machines and Statecharts

License:MIT License


Languages

Language:TypeScript 99.3%Language:JavaScript 0.7%