State Driven Development for User Interfaces (Part 2: Finite State Machines)

nimmo

Nimmo

Posted on February 4, 2018

State Driven Development for User Interfaces (Part 2: Finite State Machines)

Note: This post assumes a basic familiarity with the way Redux works, although the core concept doesn't really lose anything without that understanding. Still, it might be worth checking out Explain Redux like I'm five if you are scratching your head in the second section. I'll also be using React, but the idea being presented here doesn't require React.

In order to implement the technique discussed in my previous post, it's especially helpful to be able to think about our applications in terms of a Finite State Machine.

For anyone unfamiliar with FSMs, as the name suggests, they can only have a finite number of possible states, but crucially can only be in one of those states at any given time.

Consider for example, a door. How many states could it be in? It probably initially looks something like this:

LOCKED
UNLOCKED
OPENED
CLOSED
Enter fullscreen mode Exit fullscreen mode

That's definitely a finite list of possible states our door can be in, but you may have noticed that we've made a mistake here. Do we really need a separate state for CLOSED and UNLOCKED? Well, if we're looking to be able to say that our door can only be in one of a finite number of states, then I'd say we probably don't. We can assume that CLOSED means UNLOCKED, since we know our door can't (meaningfully) be LOCKED and OPENED at the same time. So perhaps our states should look more like this:

LOCKED
CLOSED
OPENED
Enter fullscreen mode Exit fullscreen mode

Now we've figured out our states, we'd probably like to know how our door will transition from one to another, right?

Here's a very simple state transition diagram for our door:

Diagram showing LOCKED -> Unlock -> CLOSED -> Open -> OPENED transitions

In this case, the initial state doesn't matter so much (by which I mean any of these states would have been fine as the initial state), but let's say that the initial state of our door is going to be CLOSED.

And, you know what, we don't really care about the transitions that just go back to their previous state either, do we? They're all just showing actions that aren't available in the current state, after all:

Diagram showing LOCKED -> Unlock -> CLOSED -> Open -> OPENED transitions

Now, we don't really spend a lot of time at work building virtual doors, but let's say that we think we've identified a gap in the market, and we were looking to fill it by building our door into a web application.

We've already done the first step: figuring out our states and our transitions. Now it's time for a little bit of code.

Enter Redux

Saying "Redux isn't necessary for this" is, of course, redundant. But since it just happens to be perfect for what we're trying to achieve here, that's what we'll be doing. So, we can take our diagram, and use that to write our store file:

export
const actionTypes = {
  OPEN: 'OPEN',
  CLOSE: 'CLOSE',
  LOCK: 'LOCK',
  UNLOCK: 'UNLOCK',
};

export
const stateTypes = {
  OPENED: { 
    name: 'OPENED', 
    availableActions: [actionTypes.CLOSE] 
  },
  CLOSED: { 
    name: 'CLOSED', 
    availableActions: [actionTypes.OPEN, actionTypes.LOCK] 
  },
  LOCKED: { 
    name: 'LOCKED', 
    availableActions: [actionTypes.UNLOCK] 
  },
};

const initialState = {
  _stateType: stateTypes.CLOSED,
};

export
const open = 
  () => ({ 
    type: actionTypes.OPEN,  
  });

export
const close =
  () => ({ 
    type: actionTypes.CLOSE,  
  });

export
const lock =
  () => ({ 
    type: actionTypes.LOCK,  
  });

export
const unlock =
  () => ({ 
    type: actionTypes.UNLOCK,  
  });

const door =
  (state = initialState, action) => {
    const actionIsAllowed =
      state._stateType.availableActions.includes(action.type);

    if(!actionIsAllowed) return state;

    switch(action.type) {
      case actionTypes.OPEN: 
        return { _stateType: stateTypes.OPENED };

      case actionTypes.CLOSE:
      case actionTypes.UNLOCK:
        return { _stateType: stateTypes.CLOSED };

      case actionTypes.LOCK:
        return { _stateType: stateTypes.LOCKED };

      default: 
        return state;
    }
  };


export default door;
Enter fullscreen mode Exit fullscreen mode

Now we have our reducer, which is a coded version of our state transition diagram. Did you notice how easy it was to go from the diagram to the code here? Of course, the level of complexity in this example is very low, but I'm hoping you can see why we're finding this so useful.

The only thing that's in here that's "unusual" is the use of _stateType, which you can see also contains a list of available actions in a given state. The usefulness of this might be questionable, but I believe that it offers both an extra level of documentation for the reader of this code, as well as a potential safety net against errors when transitioning from one state to another.

Implementation

Wiring this together into a container to hold our door, it looks like this:

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { 
  stateTypes,
  close as closeFunction,
  open as openFunction,
  lock as lockFunction,
  unlock as unlockFunction, 
} from './path/to/store';

import OpenedDoor from './path/to/opened_door';
import ClosedDoor from './path/to/closed_door';
import LockedDoor from './path/to/locked_door';

const Door = 
  ({ 
    _stateType, 
    open,
    close,
    lock,
    unlock,
  }) => {
    switch(_stateType) {
      case stateTypes.OPENED:
        return (
          <OpenedDoor 
            close={close} 
          />);

      case stateTypes.CLOSED: 
        return (
          <ClosedDoor 
            open={open} 
            lock={lock}
          />);

      case stateTypes.LOCKED:
        return (
          <LockedDoor 
            unlock={unlock}
          />);

      default: 
        return null;
    }
  };

const mapStateToProps = 
  ({ door }) => ({
    _stateType: door._stateType,
  });

const mapDispatchToProps =
  dispatch => 
    bindActionCreators(
      {
        open: openFunction,
        close: closeFunction,
        lock: lockFunction,
        unlock: unlockFunction,
      }, dispatch);

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(Door);
Enter fullscreen mode Exit fullscreen mode

Essentially, containers are rendered in exactly the same way as actions are processed in our reducer; a switch statement on the stateType returns the correct child component for a given state.

And from here, we'll have individual stateless components for each of our "door" types (open/closed/locked), which will be rendered to the user depending on the state the door is in, and will only allow for actions that are available based on our original state transition diagram (go and double check; they should match up nicely).

It's worth noting that the fact that the actual rendering of components almost feels like an afterthought isn't a coincidence (so much so that I didn't even feel that showing the code for the components themselves would add any value to this post, but you can view them on Github if you feel otherwise). Thinking about state above all else lends itself to easy planning, to the point where actually putting it together is really simple. This methodology really is all about promoting more thought up-front; although the benefits are more obvious in a more complicated application than our door.

In the next part we'll look at how to expand this to be more usable in a real application, by introducing a methodology for dealing with parallel state machines.

💖 💪 🙅 🚩
nimmo
Nimmo

Posted on February 4, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related