Skip to content

tomByrer/state-shifter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

state-shifter(s)

About

JavaScript Finite State Machines that are simple to use to simplify your code, while powerful and flexible

Finite State Machines (FSM) helps organize your code and prevent errors by preventing a complex web of if else / switch statements. If you picture your code as a flowchart with several branches, then a FSM is for you.

state-shifter seeks to make FSMs easy to learn, fun to build, while maximizing JavaScript's power and flexibility. It is not a full DSL and ecosystem like XState. Rather it is only there to progress though the state (& run the functions) you define. state-shifter code is very minimal, with only a few conventions to remember.

Example

Is your code messy JavaScript/Typescript code full of if/else/switch/case statements? with state-shifter, you can reduce the complex down to an easy to scan object that is traversed by a short script. Here we take 65 lines web of JS reduced to 25 lines thanks to simple-state-shifter.

The simple-state-shifter FSM is a short clean object while the same result in plain JavaScript requires many statements! FSMs can help with development and debugging speed. See for yourself!

comparison of plain JavaScript code (65 lines) versus simple-state-shifter (25 lines of code (theme: tawny-owl)

Click to open for more detail of this example: A client wants you to build a 'countdown timer' (sometimes found as Pomodoro timer). He wants it to have the following modes:
  • setting (enter timer length)
  • running (time is counting down)
  • paused (temporary pause)
  • alarm (time expired)
  • standby (timer is reset to start)

Not all of these modes are to be accessible to each other; only a few triggers will transition to another mode (AKA 'state'). So you produce this lovely MermaidJS diagram:

stateDiagram-v2
  [*] --> setting
  setting --> running : start
  running --> setting:delete
  running --> alarm:expire
  running --> paused:pause
  running --> standby:reset
  paused --> setting:delete
  paused --> standby:reset
  paused --> running:resume
  alarm --> setting:delete
  alarm --> standby:stop
  standby --> setting:delete
  standby --> running:start

Loading

flowchart of countdown timer

The above diagram translates to this code:

import createMachine from '../simple-state-shifter'
const states ={
 setting: { // default 1st screen, no timer set
  start: 'running',
  },
 running: {
  delete: 'setting',
  expire: 'alarm', // countdown reached 0
  pause: 'paused', // halt countdown, current value is on hold
  reset: 'standby', // stop countdown, return to after the time is set
  },
 paused: {
  delete: 'setting',
  reset: 'standby',
  resume: 'running',
  },
 alarm: {
  delete: 'setting',
  reset: 'standby', // AKA 'stop the alarm'
  },
 standby: { // timer reset, awaiting to start
  delete: 'setting',
  start: 'running',
  },
}
export const machine = createMachine(states)

Note, this is non-functioning 'timer'. To get it to work, you have to change each target into a function that you run, and return the next state, like this:

export const states ={
  setting: {  // 1st screen, no timer set
    start: (sec)=>{
      sec ||= data.get('defaultSeconds')
      if (sec > 0){
        FN.start(sec)
        return 'running'
      }},
  },
  running: {
    delete: ()=>{FN.pause();FN.delete();return 'setting'},
    expire: ()=>{FN.pause();return 'alarm'}, // countdown reached 0
    pause: ()=>{FN.pause();return 'paused'}, // stop countdown, current value is on hold
    reset: ()=>{FN.pause();FN.reset();return 'standby'}, // stop countdown, return to after the time is set
  },
  paused: {
    delete: ()=>{FN.delete();return 'setting'},
    reset: ()=>{FN.reset();return 'standby'},
    resume: ()=>{FN.resume();return 'running'},
  },
  alarm: {
    delete: ()=>{FN.delete();return 'setting'},
    reset: ()=>{FN.reset();return 'standby'},// note pause = stop
  },
  standby: { // timer reset, awaiting to start
    delete: ()=>{FN.delete();return 'setting'},
    start: ()=>{FN.start();return 'running'},
  },
}

Usage

createMachine function parameters:

  • definition [manditory]: object of states & triggers
    • the target of triggers may be the next state or a function that returns the next state if applicable
  • data [optional]: where 'state' and 'context' are held. Default: new Map([['state','']])
  • stateID [optional]: key name in data that holds the FSM current state. Default: state

Methods:

  • machine.trigger: AKA event: pass the string to trigger a transition or function call
  • machine.getState : returns current state (position) of the Finite State Machine
  • machine.getTriggers: returns an array of triggers you can use to change the current state or to activate functions

Conventions

  • instantiated FSM is named 'machine' : export const machine = createMachine(states)
  • if you use an external data source, the FSM current state key is named state
  • Each state is a key within states={}. Each sub-objects have transitions (AKA triggers) listed as keys, with their values are the destination states. Easy for everyone to read!
  • states are usually nouns, triggers are usually verbs or commands like 'next'
  • guard conditions : replace the value of the trigger with a function.

Advanced

Bring another state-storage

  • Default: internal js Map() with a key of state
    • get and set are used internally to change the state
    • value is returned as data for the returned machine
  • you may override the state-storage by passing it as a second parameter
    • your state-storage must have methods for get(keyname) and set(keyname, value)
    • declare your base state & context names & defaults in the presets=[ [key, value],... ] array
    • by convention / default, the base state for the FSM has a key name of state
      • if you want to use a different key name for the base state, pass it as a third parameter
  • be aware external state-storage data can be accessed for read & write outside of the FSM:
    • pros (ease of use)
    • cons (changes outside of FSM)
  • successful third-party Map replacement libraries used:

Edit the states object

  • When you edit the states object that you fed to create the FSM (eg let machine = createMachine(states, data)), any changes you made will not happen unless you restart the FSM.

simple-state-shifter vs XState

XState is more powerful out-of-the-box than simple-state-shifter. That extra power does come with a steeper learning curve, and uses more memory and CPU to run.

XState simple
main focus actors finite state machines
package size ~100KB <1KB
features many very few; aims to be small, fast, & modular
context yes solution: external or custom Map()-like data
parallel states yes solution: run multiple machines
parent/child yes solution: flatten &/or run multiple machines
initial state yes first state is always initial
final state tag yes solution: define 'final' state sans transactions
actions yes solution: "inline function" instead of plain transition
guards yes solution: "inline function" conditionally returning new state
state snapshots yes no; but can build custom external function
website great no; just GitHub README & examples

TODO

  • explore to see if need state onEnter function again
  • Mermaid.js to state
  • deluxe-state-shifter

License

(C)2025-2026 Tom Byrer, rights reserved, but ask me about OSS / usage License

About

JS Finite State Machines made simple

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published