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.
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!
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
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'},
},
}- 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
datathat holds the FSM current state. Default:state
machine.trigger: AKA event: pass the string to trigger a transition or function callmachine.getState: returns current state (position) of the Finite State Machinemachine.getTriggers: returns an array of triggers you can use to change the current state or to activate functions
- 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.
- Default: internal js Map() with a key of
stategetandsetare used internally to change the state- value is returned as
datafor 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)andset(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
- your state-storage must have methods for
- 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:
- tomByrer/alien-signals-mapish to automatically update the countdown timer
- online demo uses another ReactJS wrapper for alien-signals to add reactivity.
- 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.
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 |
- explore to see if need state
onEnterfunction again - Mermaid.js to state
- deluxe-state-shifter
(C)2025-2026 Tom Byrer, rights reserved, but ask me about OSS / usage License

