DEV Community

Linas Spukas
Linas Spukas

Posted on

Finite State Machine in JavaScript

A finite state machine is an abstract machine that can have only a fixed number of states. That means it has a fixed number of inputs and a set of outputs. A keyword finite represents that it will have limited options and memory to play with.
A state machine takes input and generates output - a new state:

newState = input + currentSate
Enter fullscreen mode Exit fullscreen mode

A simple example of a finite state machine could be a regular flashlight with one button. It has one input - a button. When you press it, the state changes from OFF to ON, and you see the light. When you press the same button again, it will change to OFF.
A single input can produce multiple states. Think of flashlight with several modes. On the first button press, you see the light; on the second press, the light starts to blink, and pressing the same button again, it'll switch off.
Such state transitions can be represented in the table:

Input Current state Next state
PRESS OFF ON
PRESS ON BLINK
PRESS BLINK OFF

Structure

A state machine should be defined by the following properties:

  • Initial state
  • Actions for state transition
  • Method to dispatch actions

The initial state is a default state when you initiate your machine. In the flashlight example, the initial state is OFF.

const machine = {
    state: 'OFF',
    ...
};
Enter fullscreen mode Exit fullscreen mode

Actions define state transitions. Each action tells what should happen when it is invoked in a current state. For example, if the flashlight state was OFF, and we dispatch a PRESS action, then the machine will look at the current state, which is OFF, search for the defined actions, which is press() and invokes it. This will transition state from OFF to ON:

const machine = {
    state: 'OFF',
    transitions: {
        OFF: {
            press() {
                this.state = 'ON'
            }
        },
        ON: {
            press() {
                this.state = 'BLINK';
            },
        },
        BLINK: {
            press() {
                this.state = 'OFF';
            },
        },
    },
    ...
};
Enter fullscreen mode Exit fullscreen mode

To pass actions, we need a method. This method takes action name as an argument (additional arguments are optional if state logic is more complex ). When a dispatch method invoked, it looks in transitions, current state, searches for the dispatched action, and triggers it.

const machine = {
    ...
    dispatch(actionName) {
        const action = this.transitions[this.state][actionName];

        if (action) {
            action.call(this);
        } else {
            console.log('Invalid action');
        }
    },
};
Enter fullscreen mode Exit fullscreen mode

Putting it all together, we have a straightforward state machine that defines a sequence of state transitions depending on actions.

const machine = {
    state: 'OFF',
    transitions: {
        OFF: {
            press() {
                this.state = 'ON'
            }
        },
        ON: {
            press() {
                this.state = 'BLINK';
            },
        },
        BLINK: {
            press() {
                this.state = 'OFF';
            },
        },
    },
    dispatch(actionName) {
        const action = this.transitions[this.state][actionName];

        if (action) {
            action.call(this);
        } else {
            console.log('invalid action');
        }
    },
};

const flashlight = Object.create(machine);

console.log(flashlight.state); // OFF
flashlight.dispatch('press'); 
console.log(flashlight.state); // ON
flashlight.dispatch('press');
console.log(flashlight.state); // BLINK
Enter fullscreen mode Exit fullscreen mode

We have created a new flashlight state object from the machine to inherit all the properties and methods. Then we dispatched action, named 'press', which triggered a response for a state change.
To invoke action function in dispatch we used action.call(this). A method Function.call() provides a context (this) for an action function, that refers to a newly created flashlight object.

Summing up

Finite state machines let you control the flow of your application state. It defines what should happen when specific actions are determined during the current state and make your application less error-prone.

Top comments (4)

Collapse
 
mikenikles profile image
Mike

This is a great introduction Linas, thanks for sharing. I recently started to work with finite state machines in my project and use xstate.js.org.

If people find state machines interesting, XState is a good next step I think.

Collapse
 
crazyoptimist profile image
crazyoptimist • Edited

Very clean explanation! Thanks for sharing!

I think this is better for readibility:

dispatch: function(actionName) {
  // body
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
desibabe profile image
Desibabe

I am learning typescript, how do I convert this into ts? Thanks.

Collapse
 
viniciusfxavier profile image
Vinícius Francisco Xavier • Edited

State Transitions Flexibility: TypeScript Implementation

// machine.d.ts
declare type TransitionFunction = () => void;

declare type Transition = {
    [key: string]: TransitionFunction;
};

declare type State = {
    [key: string]: Transition;
};

declare type Machine = {
    state: string;
    transitions: State;
    dispatch: (actionName: string) => void;
};

// main.ts
import { Machine } from "./machine";

const machine: Machine = {
    state: 'OFF',
    transitions: {
        OFF: {
            press() {
                this.state = 'ON';
            }
        },
        ON: {
            press() {
                this.state = 'BLINK';
            }
        },
        BLINK: {
            press() {
                this.state = 'OFF';
            }
        }
    },
    dispatch(actionName: string) {
        const action = this.transitions[this.state][actionName];
        if (action) {
            action.call(this);
        } else {
            console.log('invalid action');
        }
    }
};

const flashlight: Machine = Object.create(machine);

console.log(flashlight.state); // OFF
flashlight.dispatch('press');
console.log(flashlight.state); // ON
flashlight.dispatch('press');
console.log(flashlight.state); // BLINK
Enter fullscreen mode Exit fullscreen mode

State Transitions No Flexible but Safety Typed: TypeScript Implementation

// machine.d.ts
export enum State {
    OFF = 'OFF',
    ON = 'ON',
    BLINK = 'BLINK'
}

declare type TransitionFunction = () => void;

declare type Transition = {
    [key in State]: TransitionFunction;
};

declare type Machine = {
    state: State;
    transitions: Transition;
    dispatch: (actionName: State) => void;
};

// main.ts
import { State, Machine } from "./machine";

const machine: Machine = {
    state: State.OFF,
    transitions: {
        [State.OFF]: {
            press() {
                this.state = State.ON;
            }
        },
        [State.ON]: {
            press() {
                this.state = State.BLINK;
            }
        },
        [State.BLINK]: {
            press() {
                this.state = State.OFF;
            }
        }
    },
    dispatch(actionName: State) {
        const action = this.transitions[this.state][actionName];
        if (action) {
            action.call(this);
        } else {
            console.log('invalid action');
        }
    }
};

const flashlight: Machine = Object.create(machine);

console.log(flashlight.state); // OFF
flashlight.dispatch(State.ON);
console.log(flashlight.state); // ON
flashlight.dispatch(State.BLINK);
console.log(flashlight.state); // BLINK
Enter fullscreen mode Exit fullscreen mode