React and Finite State Machines

Akshay666n/ January 16, 2020/ Engineering, Functional Programming, Web

Finite state machines, generally speaking, are a computation model used to describe a system in terms of the transitions between it’s states. They make it possible to is really easy to visualize your computational model as a graph and make sure that your application isn’t left with unhandled cases or illegal states.


Warning: Read the wikipedia page on Finite State Machines at your own risk. Side-effects of a visit to that page includes mild headaches, an annoying twitch in your eye and even death. If you’re a nerd, you are immune to the side-effects so knock yourself out.
I am not gonna get into the theory bit of FSMs because it gets super mathematical, really fast. Also, I don’t know much about it. So here’s a more practical take on FSMs.

Before we get into any libraries, let’s implement a Finite State Machine ourselves and try to understand what they mean. FSMs are about two things – Finite set of states and finite set of valid transitions between them.


What Is ‘Finite set of states’?

The state of any system should be predictable. We should design our systems in such a way that the number of combinations of states are minimized so that there are fewer cases left to handle.

Lets take a traffic signal an example:

const trafficLight = {
  isRed: true,
  isGreen: false,
  isYellow: false,
};

Here, you have 3 flags describing the current state of a traffic signal. But what happens when isRed and isGreen are both true. The total number of combinations you have to handle is 6. Is that state valid? Have you handled this possibility in your application? Should you handle it? The answer to all of them is a strong NO in a bold font.

Well, that sucks. What if, we had one piece of state instead?

type TrafficSignal = { color: 'red' | 'green' | 'yellow' };
const trafficSignal: TrafficSignal = {
  color: 'green',
};

This is a lot simpler and it makes sure that there is no invalid state for the traffic light color. The light will either be red or green or yellow. Which means you only have 3cases to handle in your application i.e. less code. Life hacked! Thank you.


What is ‘State transitions’?

This is the second and the most crucial part about FSMs. As you may have seen with useState in react, you get the current state and a state setter to manage a piece of state. Let’s enhance that.

Let’s make a state setter

type TrafficLight = 'red' | 'green' | 'yellow';
let state: TrafficLight = 'red';
const setState = (light: TrafficLight) => (state = light);

In this system, you cannot have an invalid state i.e. light will always be either redgreen or yellow. So everything’s awesome, right? WRONG! What if the state was green and someone called setState(‘red’)? That is an invalid state transition as it skips over the yellowstate that traffic lights are expected to go through. How do we solve that?

const getNextState = (currentState: TrafficLight): TrafficLight =>
  ({
    green: 'yellow',
    yellow: 'red',
    red: 'green',
  }[currentState]);

const nextState = () => setState(getNextState(state));

Now this system is very strict about the number of valid states and it has a finite set (only 1) of valid transitions. WE HAVE A FINITE STATE MACHINE!

You can visualize the state machine as,


Yeah, cool cool. What do I do with this?

FSM gives us a very predictable, declarative, readable and elegant system. Also, my tiny little brain can’t keep track of too many pieces of states simultaneously so I find finite state machines really helpful in making sense of my own code. Obviously the example above is not all that powerful but it presents us with a model that will allow us to write code that we’re sure about and is one step towards writing cleaner, more reliable code.


I wanna see more code. Show me the code.

Alrighty then.

There are a few good FSM libraries on npm. The most popular one amongst them is xstate. Here’s the traffic lights example using xstate.

The API used to defined states and its transitions is called a statechart.

const stateChart = {
  id: 'trafficLight',
  initial: 'green',
  states: {
    green: {
      on: { NEXT: 'yellow' },
    },
    yellow: {
      on: { NEXT: 'red' },
    },
    red: {
      on: { NEXT: { target: 'green' } }, // Same as the ones before
    },
  },
};

This state describes the valid states as being ‘green’‘yellow’ and ‘red’ and the only available transition is NEXT. The states can be read as “If its green right now, on NEXT it’ll transition to target yellow

Here’s how you use xstate,

import { Machine } from 'xstate';

// Create a state machine
const trafficLight = Machine(stateChart);

// Trigger the `NEXT` transition from `green` to the next state
const { value: nextLight } = lightMachine.transition('green', 'NEXT');

// If `green`, on `NEXT` transition to `yellow`
expect(nextLight).toBe('yellow');

Shoutout to all the react nerds in the audience! Here’s a simple hooks example to get you hooked on state machines.

import { Machine } from 'xstate';

function useStateMachine(machine) {
  const [state, setState] = useState(machine.value);

  function dispatch(transition) {
    const { value: nextState } = machine.transition(state, transition);
    setState(nextState);
  }

  return { state, dispatch };
}

Here’s how you use this hook,

const trafficMachine = Machine({
  id: 'trafficLight',
  initial: 'green',
  states: {
    green: {
      on: { NEXT: 'yellow' },
    },
    yellow: {
      on: { NEXT: 'red' },
    },
    red: {
      on: { NEXT: 'green' },
    },
  },
});

function TrafficLights() {
  const { state, dispatch } = useStateMachine(trafficMachine);

  const lightColor = {
    green: '#51e980',
    red: '#e74c3c',
    orange: '#ffa500',
  }[state];

  return (
    <>
      <div className="trafficLight" style={{ backgroundColor: lightColor }} />
      <div>The light is {state}</div>
      <button onClick={() => dispatch('NEXT')}>Next light</button>
    </>
  );
}

useTinyStateMachine

XState has a lot of features so the minzipped size is around 15KB. Not everyone can justify the added payload. Which is why I wrote a lighter alternative use-tiny-state-machine which is a ~700 bytes (minzipped) react hook.

import useTinyStateMachine from 'use-tiny-state-machine';

const stateChart = {
  id: 'trafficLight',
  initial: 'green',
  states: {
    green: {
      on: { NEXT: 'yellow' },
    },
    yellow: {
      on: { NEXT: 'red' },
    },
    red: {
      on: { NEXT: 'green' },
    },
  },
};

function TrafficLights() {
  const { cata, state, dispatch } = useTinyStateMachine(stateChart);

  // cata let's you reduce the current state to a value or a function call
  const lightColor = cata({
    green: '#51e980',
    red: '#e74c3c',
    orange: '#ffa500',
  });

  return (
    <>
      <div className="trafficLight" style={{ backgroundColor: lightColor }} />
      <div>The light is {state}</div>
      <button onClick={() => dispatch('NEXT')}>Next light</button>
    </>
  );
}

Codesandbox demo

Here’s a simple login form page example with actions, context and multiple transitions,

import useTinyStateMachine from 'use-tiny-state-machine';

function LoginPage({ onSubmit }) {
  const { cata, dispatch } = useTinyStateMachine({
    id: 'loginForm',
    initial: 'home',
    context: {
      username: '',
    },
    states: {
      home: {
        on: {
          OPEN_FORM_LOGIN: 'loginForm',
          OPEN_FORM_SIGNUP: 'signupForm',
        },
      },
      loginForm: {
        on: {
          SUBMIT: {
            target: 'submitted', // Target state
            action: submitFormData, // The transition triggers this action
          },
          SWITCH_FORM: 'signupForm',
        },
      },
      signupForm: {
        on: {
          SUBMIT: {
            target: 'submitted',
            action: submitFormData,
          },
          SWITCH_FORM: 'loginForm',
        },
      },
      submitted: {
        onEntry: () => (window.location.href = '/dashboard'), // Redirect to wherever after login
      },
    },
  });

  function submitFormData({ updateContext }, data) {
    updateContext({ username: data.username });
    onSubmit(data);
  }

  return cata({
    home: () => (
      <>
        <button onClick={() => dispatch('OPEN_FORM_LOGIN')}>Login</button>
        <button onClick={() => dispatch('OPEN_FORM_SIGNUP')}>Signup</button>
      </>
    ),
    loginForm: () => (
      <>
        <button onClick={() => dispatch('SWITCH_FORM')}>Signup</button>
        <LoginForm onSubmit={data => dispatch('SUBMIT', data)} />
      </>
    ),
    signupForm: () => (
      <>
        <button onClick={() => dispatch('SWITCH_FORM')}>Login</button>
        <SignupForm onSubmit={data => dispatch('SUBMIT', data)} />
      </>
    ),
    submitted: ({ context: { username } }) => (
      <div>You have been logged in as {username}. Redirecting...</div>
    ),
  });
}

Codesandbox demo

Here, context (not to be confused with react’s context) is some additional data that you want associated with your state. You can use context to store information that the future states may need. The fixed set of transitions makes sure that the future state will have the required context data.

This is what the state chart looks like visually,

To visualize the state charts you make, you can use the xstate visualizer. Even though use-tiny-state-machine and xstate have slightly different apis, you can still use the visualizer pretty well.


Comparisons with useReducer

Reducers are a good step forward but actions are not handled well and the logic you end up writing for it becomes very imperative. One way to tackle this is to make the effect execution declarative as well. With that data and effect, both turn into reduce operations(state -> state; effect -> effect). Here’s how to do that…

Note: This idea was stolen from a David K. Piano tweet but I can’t find the exact tweet right now to link.

function reducer({ state }, { type, payload }) {
  switch (state.type) {
    case 'idle':
      return type === 'FETCH'
        ? {
            effect: { type: 'fetch', payload },
            state: { status: 'pending' },
          }
        : { state, effect: null };
    case 'pending':
      return {
        effect: null,
        state:
          type === 'RESOLVE'
            ? { status: 'success', data: payload }
            : { status: 'failure', error: payload },
      };
    default:
      return { state, effect: null };
  }
}

function handleAction({ effect }, dispatch) {
  switch ((effect || {}).type) {
    case 'fetch': {
      fetchMyData(effect.payload)
        .then(data => dispatch({ type: 'RESOLVE', payload: data }))
        .catch(error => dispatch({ type: 'REJECT', payload: error }));
      break;
    }
    default:
      return;
  }
}

const initialState = { state: { status: 'idle' }, effect: null };

function MyComponent({ id }) {
  const [{ state, effect }, dispatch] = useReducer(reducer, initialState);
  useEffect(() => handleAction({ state, effect }, dispatch), [effect]);

  return (
    <>
      <button onClick={() => dispatch({ type: 'FETCH', payload: { id } })}>Start fetchin</button>
      <div>
        {state.status === 'success' && 'Fetched data'}
        {state.status === 'failure' && `Error: ${state.error}`}
      </div>
    </>
  );
}

Codesandbox demo


So what does this mean for me?

Well, you can start by looking back at your old code and re-considering the application state there. Although, like a lot of things in programming, this is not a one size fits all scenario, the idea of FSM’s is pretty universal wherever you find the need for state.

xstate and use-tiny-state-machine are just tools. FSM is a concept. Even if the tools don’t fit your use case, you can still apply the concept of FSM almost everywhere.


Important links