Mastering the State Pattern: Light Switch to Finite State Machines in JS

This article explains why using if‑else for state handling violates the Open‑Closed Principle, introduces the State design pattern with a light‑switch example, demonstrates its implementation in JavaScript (including class‑based and functional FSM libraries), and compares it to the Strategy pattern.

ELab Team
ELab Team
ELab Team
Mastering the State Pattern: Light Switch to Finite State Machines in JS

Background and Scenario Description

During development we often need to switch program states and produce different behavior. Using if…else for state handling leads to many conditional branches, violating the Open‑Closed Principle, making state transitions unclear and reducing readability and extensibility.

Open‑Closed Principle (OCP): software entities should be open for extension, but closed for modification.

Example

Scenario: a bedroom light has two illumination modes—bright white and warm yellow—controlled by a single switch. First press turns on yellow light, second press switches to white light, third press turns off the light.

class Light {
  constructor() {
    this.state = 'offLightState';
    this.button = null;
  }
  init() {
    this.button = document.getElementById('btn');
    self = this;
    this.button.onclick = function() {
      self.pressed();
    };
  }
  pressed() {
    if (this.state === 'offLightState') {
      console.log('黄光');
      this.state = 'yellowLightState';
    } else if (this.state === 'yellowLightState') {
      console.log('白光');
      this.state = 'whiteLightState';
    } else if (this.state === 'whiteLightState') {
      console.log('关灯');
      this.state = 'offLightState';
    }
  };
};
const light = new Light();
light.init();

The example shows that adding or modifying states requires changing the pressed() method, making it unstable and increasingly large as states grow.

State Pattern

Definition (from Design Patterns): Allows an object to change its behavior when its internal state changes, appearing as if its class has changed.

The first part means encapsulating each state into a separate class and delegating requests to the current state object; when the internal state changes, behavior changes. The second part means from the client’s perspective the object exhibits different behavior in different states, as if instantiated from different classes.

The key of the State pattern is distinguishing the internal state of an entity; changes in internal state usually lead to behavior changes.

Implementing the Light Program with the State Pattern

Encapsulate each state into its own class, with behavior inside the class.

First define an abstract LightState class that holds a reference to the Light object and defines a pressed method. All state subclasses inherit this abstract class and must implement pressed.

abstract class LightState {
  protected light: Light;
  constructor(_light: Light) {
    this.light = _light;
  }
  abstract pressed(): void;
}

Define concrete state subclasses, each with a pressed method representing behavior when the button is pressed in that state.

// Off
class OffLightState extends LightState {
  constructor(light: Light) {
    super(light);
  }
  pressed() {
    console.log('黄光');
    this.light.setState(this.light.yellowLight);
  }
};
// Yellow
class YellowLightState extends LightState {
  constructor(light: Light) {
    super(light);
  }
  pressed() {
    console.log('白光');
    this.light.setState(this.light.whiteLight);
  }
};
// White
class WhiteLightState extends LightState {
  constructor(light: Light) {
    super(light);
  }
  pressed() {
    console.log('关闭');
    this.light.setState(this.light.offLight);
  }
};

Define a Light class that creates instances of each state and holds a reference to the current state.

Provide an init method that delegates button clicks to the current state's pressed method.

Provide a setState method to switch the Light object's state.

class Light {
  public offLight: OffLightState;
  public yellowLight: YellowLightState;
  public whiteLight: WhiteLightState;
  private currState: LightState;
  private button: any;
  constructor() {
    this.offLight = new OffLightState(this);
    this.yellowLight = new YellowLightState(this);
    this.whiteLight = new WhiteLightState(this);
    this.button = null;
  }
  init() {
    const self = this;
    this.button = document.getElementById('btn');
    this.currState = this.offLight; // initial state
    this.button.onclick = function() {
      self.currState.pressed();
    };
  };
  setState(newState: LightState) {
    this.currState = newState;
  }
};

Summary of the State pattern structure

First define the Light class (the Context). In its constructor create instances of each state class; the Context holds references to these state objects to delegate requests. Each state class receives the Light object in its constructor, allowing it to call methods or manipulate the Light object.

Applications and Pros/Cons of the State Pattern

Applications: scenarios where behavior changes with state; replaces many conditional statements related to object state.

Advantages:

The State pattern defines the relationship between states and behavior, encapsulated in classes; adding new states is easy.

Avoids a bloated Context; state transition logic is distributed among state classes, reducing conditional branches.

Requests in Context and behavior in state classes are independent.

Disadvantages:

Creates many state classes, increasing object count.

Logic is scattered across state classes, making it hard to see the whole state machine in one place.

Difference Between State and Strategy Patterns

Both avoid multiple conditionals and have similar class diagrams with a Context and multiple Strategy or State classes. Use Strategy when conditions are parallel and independent; use State when states and corresponding behaviors are predetermined and internal, such as in an IM socket scenario.

Finite State Machine

Finite State Machine (FSM) (also called finite state automaton) is a mathematical model representing a finite set of states, transitions, and actions.

Definition:

Finite state machine is a five‑tuple M = (Q, T, δ, q0, F)

Q: finite set of states (e.g., {q0, q1, q2, q3})

T: finite input alphabet (e.g., {0, 1})

δ: transition function Q × T → Q

q0: initial state

F: set of final states (e.g., {q0, q1})

JavaScript version of a state machine

The previous example simulates a traditional OO implementation of the State pattern. In JavaScript we can define a state machine object where each state is an object with a pressed method, and the Context switches the current state.

const FSM = {
  off: {
    pressed: function() {
      console.log('黄灯');
      this.currState = FSM.yellow;
    }
  },
  yellow: {
    pressed: function() {
      console.log('白光');
      this.currState = FSM.white;
    }
  },
  white: {
    pressed: function() {
      console.log('关闭');
      this.currState = FSM.off;
    }
  },
};

class Light {
  constructor() {
    this.currState = FSM.off;
    this.button = null;
  }
  init() {
    self = this;
    this.button = document.getElementById('btn');
    this.button.onclick = function() {
      self.currState.pressed.call(self);
    };
  }
}
const light = new Light();
light.init();

javascript-state-machine library

Javascript Finite State Machine is a library that makes creating FSMs easy.

const fsm = new StateMachine({
  init: 'offLight',
  transitions: [
    { name: 'pressed', from: 'offLightState', to: 'yellowLightState' },
    { name: 'pressed', from: 'yellowLightState', to: 'whiteLightState' },
    { name: 'pressed', from: 'whiteLightState', to: 'offLightState' },
  ],
  methods: {
    onBeforePressed: function() {},
    onLeaveOffLightState: function() {},
    onYellowLightState: function() {},
    onPressed: function() { console.log(fsm.state); },
    // ...
  }
});
const btn = document.getElementById('btn');
button.onclick = function() {
  fsm.pressed();
};

Key methods: fsm.state, fsm.pressed(), lifecycle hooks (onBeforeTransition, onLeaveState, onEnterState, etc.), and utility methods ( fsm.is(s), fsm.can(t), fsm.cannot(t), fsm.transitions(), fsm.allTransitions(), fsm.allStates()).

XState

XState supports React + TypeScript. Example:

import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';

const lightMachine = createMachine({
  id: 'light',
  initial: 'offLightState',
  states: {
    offLightState: { on: { Pressed: 'yellowLightState' } },
    yellowLightState: { on: { Pressed: 'whiteLightState' } },
    whiteLightState: { on: { Pressed: 'offLightState' } },
  }
});

export const Light = () => {
  const [state, send] = useMachine(lightMachine);
  return (
    <button onClick={() => send('Pressed')}>
      {state.value}
    </button>
  );
};

Key concepts: id, initial, states, on (transitions), context, and the useMachine hook returning [state, send, service].

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsJavaScriptfrontend developmentState PatternFinite State Machine
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.