DEV Community

Nimmo
Nimmo

Posted on • Updated on

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.

Discussion (6)

Collapse
dallgoot profile image
dallgoot

Can the "LOCKED" be considered a "sub" state of "CLOSED" ?
like

  • CLOSED with (UNLOCKED or LOCKED)
  • OPEN (with UNLOCKED implied) Do you have any principles on dealing with this case ?
Collapse
nimmo profile image
Nimmo Author

Good question! :-) It all comes down to how you decide to model your states really, but I imagine that you'd end up with the same thing. If "CLOSED" is a state and "UNLOCKED / LOCKED" are sub-states of "CLOSED", then essentially "CLOSED + LOCKED" and "CLOSED + UNLOCKED" both become composite states themselves, which kind of puts you back where we started with three states (since "CLOSED" must be either locked or unlocked).

Do you see what I mean? :-)

Collapse
dallgoot profile image
dallgoot

yes, thanks i guess that in this situation "LOCKED" is a shortcut for "CLOSED & LOCKED"

What i was thinking out loud with that question was about the intricacy of composite states as you call them and if any guidance is known to deal with them, theorytically and codewise :)

Not to push you to have an answer, just if there's some reflections on how to process : setting a hierarchy of states, linking them, etc
Some methods to approach it smartly like you did :)

i assumed that enters the realm of FSM but correct me if i'm wrong ;)

Thread Thread
nimmo profile image
Nimmo Author

Now that you mention it, perhaps my example is actually just too simple to address your question properly - my apologies, let's have a look at this in a little more depth. :)

In the third part of this series (which I realise you've already read), I talk about parallel state machines. That hinted at hierarchy but didn't explicitly discuss it, perhaps I should extend that post to include a better example. But did you notice how in that post, we moved from the concept of a door as a container, to a room instead, which contained a door and an alarm? This is showing a hierarchy like the one you're asking about (although again in a more simple manner really). Extrapolate this to to be say, a house that has multiple rooms each with their own doors, and you have parallelism and hierarchy being dealt with at the same time, but where each room is independent of one another, whilst the whole system is still only able to be in one state (made up of the combination of the states of all the rooms) at any given point.

In terms of implementing these in code, my take on this so far with React has been that each container is itself a representation of a finite state machine. The outermost App being the entire thing, and every container below that being an FSM that either has "sub" FSMs or parallel FSMs. If you match that to the diagram in part 3, which shows the room, then the room is a container in React that contains both the door and the alarm. Diagram and code for reference. :)

The key thing to think about here is that even just by asking the question, you're thinking about planning the architecture of your application at the right time - i.e. before you've started writing it! :) Changing your mind on how a system is put together is a lot easier when it's still just a diagram isn't it. :D

Thread Thread
dallgoot profile image
dallgoot

I agree : thinking in terms of FSM it 's an approach that's not only easier but more related to reality of systems, if i could say so.

From your answer i understand that, in fine, a FSM can also be looked as a group of states which implies that the "root" state are defined by the states of its "children" without having to care -i assume- about all the possible states of the entire application.

thank you for these articles and answers :)

Thread Thread
nimmo profile image
Nimmo Author

You're welcome, thank you for reading them! :-)

And yes, that's exactly it. Basically this whole thought process isn't really about building applications with FSMs, it's about recognising that your system is an FSM already (even before you've built it!) and using that realisation to allow you to better plan out your application from the start.